• 博客园logo
  • 会员
  • 周边
  • 新闻
  • 博问
  • 闪存
  • 赞助商
  • Chat2DB
    • 搜索
      所有博客
    • 搜索
      当前博客
  • 写随笔 我的博客 短消息 简洁模式
    用户头像
    我的博客 我的园子 账号设置 会员中心 简洁模式 ... 退出登录
    注册 登录

tintin7790

  • 博客园
  • 联系
  • 订阅
  • 管理

公告

View Post

TextCNN实践报错修改_01

1. DataFrame 各列长度不一致:All arrays must be of the same length

① 当时的源码
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import os
import re
import string
import nltk

from nltk.corpus import stopwords
from nltk.tokenize import word_tokenize
from nltk.stem import PorterStemmer
import seaborn as sns
from wordcloud import WordCloud
import time 
import copy
import torch
from torch import nn
import torch.nn.functional as F
import torch.optim as optim
from torchvision import transforms
from torchtext import data
from torchtext.vocab import Vectors,GloVe
from sklearn.model_selection import train_test_split

#nltk.download("stopwords")
data_path="./IMDB_Dataset.csv"

data_Frame=pd.read_csv(data_path)
X=data_Frame["review"].values
print(X.shape)#(50000,)
print(type(X))#<class 'pandas.arrays.StringArray'>
#print(X)

y=data_Frame["sentiment"].map({
    "positive":1,
    "negative":0,
}).values

X_train, X_test, y_train, y_test = train_test_split(
    X,
    y,
    test_size=0.25,
    random_state=42,
    stratify=y
)

def text_preprocess(text_data):
    text_pre=[]
    for text1 in text_data:
        text1=re.sub("<br /><br />"," ",text1)
        text1=text1.lower()
        text1=re.sub("\d+","",text1)
        text1=text1.translate(str.maketrans("","",string.punctuation.replace("'","")))
        text1=text1.strip()
        text_pre.append(text1)
    return np.array(text_pre)
train_text_pre1=text_preprocess(X_train)
test_text_pre1=text_preprocess(X_test)

def stop_stem_word(datalist,stop_words):
    datalist_pre=[]
    for text in datalist:
        text_words=word_tokenize(text)
        text_words=[w for w in text_words if not w in stop_words]
       #text_words=[w for w in text_words if len(re.findall("'",w))==0] ????
        datalist_pre.append(text_words)
    return datalist_pre
stop_words=stopwords.words("english")
stop_words=set(stop_words)
train_text_pre2=stop_stem_word(train_text_pre1,stop_words)
test_text_pre2=stop_stem_word(test_text_pre1,stop_words)
#print(train_text_pre1[10000])

#print(train_text_pre2[10000])

texts=[" ".join(w) for w in train_text_pre2]
traindata_save=pd.DataFrame({
    "text":texts,
    "label":y_train,
})
#print(len(texts))
#print(len(y_train))
#print(traindata_save.head())

tests=[" ".join(w) for w in test_text_pre2]
testdata_save=pd.DataFrame({
    "text":tests,
    "label":y_test,
})

#traindata_save.to_csv("./TC_textdata/imdb_train.csv")
#testdata_save.to_csv("./TC_textdata/imdb_test.csv")



traindata=pd.DataFrame({"train_text":X,"train_word":train_text_pre2,
                        "train_label":y_train})
train_word_sum=[len(text) for text in train_text_pre2]
traindata["train_word_sum"]=train_word_sum
plt.figure(figsize=(8,5))
_=plt.hist(train_word_sum,bins=100)
plt.xlabel("word number")
plt.ylabel("Freq")
plt.show()
② 报错信息
/Users/tintin7790/Code/Python/2026_08_19_BasePT/TCPractise.py:52: SyntaxWarning: invalid escape sequence '\d'
  text1=re.sub("\d+","",text1)
(50000,)
<class 'pandas.arrays.StringArray'>
Traceback (most recent call last):
  File "/Users/tintin7790/Code/Python/2026_08_19_BasePT/TCPractise.py", line 96, in <module>
    traindata=pd.DataFrame({"train_text":X,"train_word":train_text_pre2,
                            "train_label":y_train})
  File "/Library/Frameworks/Python.framework/Versions/3.13/lib/python3.13/site-packages/pandas/core/frame.py", line 769, in __init__
    mgr = dict_to_mgr(data, index, columns, dtype=dtype, copy=copy)
  File "/Library/Frameworks/Python.framework/Versions/3.13/lib/python3.13/site-packages/pandas/core/internals/construction.py", line 460, in dict_to_mgr
    return arrays_to_mgr(arrays, columns, index, dtype=dtype, consolidate=copy)
  File "/Library/Frameworks/Python.framework/Versions/3.13/lib/python3.13/site-packages/pandas/core/internals/construction.py", line 113, in arrays_to_mgr
    index = _extract_index(arrays)
  File "/Library/Frameworks/Python.framework/Versions/3.13/lib/python3.13/site-packages/pandas/core/internals/construction.py", line 643, in _extract_index
    raise ValueError("All arrays must be of the same length")
ValueError: All arrays must be of the same length

③ 错误原因与修正

pd.DataFrame({...}) 要求每一列拥有完全相同的元素数量。当时三个变量的长度分别为:

X               -> 50000    原始全部影评
X_train         -> 37500    训练集影评
train_text_pre2 -> 37500    X_train 清洗、分词后的结果
y_train         -> 37500    训练集标签

train_text_pre2[i] 是由 X_train[i] 处理得到的,并且对应 y_train[i],因此这里应该使用 X_train:

traindata=pd.DataFrame({
    "train_text":X_train,
    "train_word":train_text_pre2,
    "train_label":y_train
})

train_word_sum=[len(text) for text in train_text_pre2]
traindata["train_word_sum"]=train_word_sum

可以在创建 DataFrame 前直接检查:

print(len(X_train))
print(len(train_text_pre2))
print(len(y_train))

三者都应为:

37500
37500
37500

同时,Python 3.13 对:

re.sub("\d+","",text1)

产生:

SyntaxWarning: invalid escape sequence '\d'

正则表达式通常使用 raw string:

text1=re.sub(r"\d+","",text1)

这里 r"\d+" 表示匹配一个或多个连续数字。

④ 正确输出

len(X)                = 50000
len(X_train)          = 37500
len(train_text_pre2)  = 37500
len(y_train)          = 37500

traindata.shape
# (37500, 4)

最终四列的关系为:

train_text            train_word               train_label    train_word_sum
原始训练影评           分词后的单词列表             0/1            单词数量

2. TabularDataset 读取 CSV、表头与 Dataset 再划分问题

① 当时的源码
mytokenize=lambda x:x.split()

TEXT=data.Field(sequential=True,
                tokenize=mytokenize,
                include_lengths=True,#让迭代器同时返回文本张量和每个句子的真实长度
                use_vocab=True,#基于训练数据构建词汇表,并把单词映射成数字索引。
                batch_first=True,
                fix_length=200,)#把所有句子填充或截断到固定长度 200

LABEL=data.Field(sequential=False,
                 use_vocab=False,
                 pad_token=None,
                 unk_token=None,)

train_data,test_data=data.TabularDataset.splits(
    path="./TC_textdata",
    format="csv",
    train="imdb_train.csv",
    test="imdb_test.csv",
    fields=[
        ("text",TEXT),
        ("label",LABEL)
    ],
)

print(len(train_data),len(test_data))
ex0=train_data.examples[0]
print(ex0.label)
print(ex0.text)


train_data,val_data=train_data.splits(split_ratio=0.7)

print(train_data.shape,val_data.shape)
② 报错信息
/Users/tintin7790/Code/Python/2026_08_19_BasePT/TCPractise.py:52: SyntaxWarning: invalid escape sequence '\d'
  text1=re.sub("\d+","",text1)
(50000,)
<class 'pandas.arrays.StringArray'>
37501 12501
text
[]
Traceback (most recent call last):
  File "/Users/tintin7790/Code/Python/2026_08_19_BasePT/TCPractise.py", line 138, in <module>
    train_data,val_data=train_data.splits(split_ratio=0.7)
                        ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^
  File "/Library/Frameworks/Python.framework/Versions/3.13/lib/python3.13/site-packages/torchtext/data/dataset.py", line 76, in splits
    path = cls.download(root)
  File "/Library/Frameworks/Python.framework/Versions/3.13/lib/python3.13/site-packages/torchtext/data/dataset.py", line 169, in download

③ 错误原因与修正

这里同时出现了三个问题。

第一,原本训练集应为 37500 条、测试集应为 12500 条,但输出:

37501 12501

说明 CSV 第一行:

text,label

也被当成了一条普通数据。读取带表头的 CSV 时增加:

skip_header=True

第二,保存 CSV 时 Pandas 默认会额外写入 index。如果生成:

,text,label
0,good movie,1
1,bad movie,0

而 fields 只有两个字段,列与 Field 会发生错位。因此保存时明确:

traindata_save.to_csv(
    "./TC_textdata/imdb_train.csv",
    index=False
)

testdata_save.to_csv(
    "./TC_textdata/imdb_test.csv",
    index=False
)

这样 CSV 保持:

text,label
good movie,1
bad movie,0

第三:

data.TabularDataset.splits(...)

用于一次从多个 CSV 文件创建多个 Dataset。

已经得到一个 train_data 后,再将这个 Dataset 分成训练集与验证集,应调用实例方法:

train_data.split(...)

因此:

train_data,val_data=train_data.split(split_ratio=0.7)

同时 train_data 是 Dataset 对象,没有 .shape;Dataset 样本数量使用:

len(train_data)

完整修正:

traindata_save.to_csv(
    "./TC_textdata/imdb_train.csv",
    index=False
)

testdata_save.to_csv(
    "./TC_textdata/imdb_test.csv",
    index=False
)

train_data,test_data=data.TabularDataset.splits(
    path="./TC_textdata",
    format="csv",
    train="imdb_train.csv",
    test="imdb_test.csv",
    fields=[
        ("text",TEXT),
        ("label",LABEL)
    ],
    skip_header=True,
)

print(len(train_data),len(test_data))

ex0=train_data.examples[0]
print(ex0.label)
print(ex0.text)

train_data,val_data=train_data.split(split_ratio=0.7)

print(len(train_data),len(val_data))

对象关系:

TEXT / LABEL        -> Field 对象,描述对应列如何处理
TabularDataset      -> 按 Field 规则读取 CSV
train_data          -> Dataset 对象
train_data.examples -> Dataset 内保存的 Example 列表
examples[0]         -> 一条 Example
ex0.text            -> 当前样本的文本 token
ex0.label           -> 当前样本的标签

④ 正确输出

重新生成 CSV 并使用 skip_header=True 后:

37500 12500

ex0 会是一条真正的数据,例如:

1
['movie', 'really', 'good', ...]

再次划分:

train_data,val_data=train_data.split(split_ratio=0.7)

输出约为:

26250 11250

不同版本的 torchtext 在整数划分上可能出现一条样本的舍入差异,例如:

26251 11250

均属于正常现象。


3. text 与 label 读反:CSV 实际列顺序与 fields 对应错误

① 当时的源码
ex0=train_data.examples[2]
print(ex0.label)#
print(ex0.text)#
② 当时的错误输出
/Users/tintin7790/Code/Python/2026_08_19_BasePT/TCPractise.py:52: SyntaxWarning: invalid escape sequence '\d'
  text1=re.sub("\d+","",text1)
(50000,)
<class 'pandas.arrays.StringArray'>
37501 12501
one shamelessly enjoyed every episode pushing daisies season hope writers ' strike wo n't brutally end beginnings good show ned pie maker owns restaurant middle town secret talent emmerson private investigator unique quirks like love knitting charlotte chuck oncedeadbutnotanymore childhood friend sunny spot ned 's life olive jealous goodhearted waitress oh add dog jim dale brings characters together wonderful narration show chuck ned emmerson along olive occasionally dog solve multiple murder mysteries assistance ned 's special gift bringing dead people back life show funny clean romantic cute goodhearted way 'd recommend anyone
['1']
26251 11250

③ 错误原因与修正

当时:

print(ex0.label)

输出了一整篇影评,而:

print(ex0.text)

输出:

['1']

说明 torchtext 按照 CSV 列位置绑定 fields 时,实际文件结构与:

fields=[
    ("text",TEXT),
    ("label",LABEL)
]

没有对应起来。

TabularDataset 主要根据列的位置解释 Field:

CSV 第 1 列 -> ("text", TEXT)
CSV 第 2 列 -> ("label", LABEL)

因此需要重新生成 CSV,确保文件真实结构为:

text,label
影评内容,1
影评内容,0

对应代码:

traindata_save=pd.DataFrame({
    "text":texts,
    "label":y_train,
})

testdata_save=pd.DataFrame({
    "text":tests,
    "label":y_test,
})

traindata_save.to_csv(
    "./TC_textdata/imdb_train.csv",
    index=False
)

testdata_save.to_csv(
    "./TC_textdata/imdb_test.csv",
    index=False
)

然后重新读取:

train_data,test_data=data.TabularDataset.splits(
    path="./TC_textdata",
    format="csv",
    train="imdb_train.csv",
    test="imdb_test.csv",
    fields=[
        ("text",TEXT),
        ("label",LABEL)
    ],
    skip_header=True,
)

需要注意:修改 Python 代码并不会自动改变之前已经写入硬盘的旧 CSV。因此 CSV 列结构出错时,需要真正重新执行 to_csv() 覆盖旧文件。

TEXT 定义:

TEXT=data.Field(
    sequential=True,
    tokenize=mytokenize,
    ...
)

所以 ex0.text 会被 tokenize 成:

list[str]

LABEL 定义:

LABEL=data.Field(
    sequential=False,
    ...
)

所以 ex0.label 保存单个标签。

④ 正确输出

修正并重新生成 CSV 后:

ex0=train_data.examples[2]

print(ex0.label)
print(ex0.text)

应得到类似:

1
['movie', 'journey', 'mind', 'screenwriter', 'caught', 'paradoxical', 'philosophy', 'examines', 'ever', 'illusive', 'question', "'", "'", 
"'", "'", "'s", 'courageous', 'thought', 'provoking', 'enterprise', 'shipload', 'beautiful', 'images', 'dreaminspired', 'escherlike', 'paradoxes',
'reminiscent', 'hand', 'drawing', 'rather', 'erasing', 'follow', 'writer', 'agony', 'say', 'film', 'see', 'phoning', 'wife', 'left', 'peru', 
'leaving', 'take', 'care', 'baby', 'task', 'performs', 'less', 'less', 'attention', "'s", 'absorbed', 'dilemma', "'s", 'hardly', 'looks',
'child', 'anymore', 'wife', 'comes', 'back', 'makes', 'scene', 'destroys', 'notes', 'helping', 'go', 'last', 'treshold', 'erases', 
'interspersed', 'eyepleasing', 'idestructing', 'images', 'story', 'mainly', 'philosophical', "'s", 'veils', 'maya', 'world', 'illusion',
'paradox', 'movie', 'however', 'needs', 'lot', 'talking', 'thinking', 'prove', 'thinking', 'stop', 'two', 'hours', 'provocative', 'beauty', 
'rapid', 'philosophising', 'movie', 'made', 'long', 'silence', 'shorter', 'movie', 'purpose', 'maker', 'succeeded', 'quite', 'well']

此时对象关系正确:

ex0
├── ex0.text  -> list[str],一条影评分词后的 token
└── ex0.label -> 0 / 1,情感标签

4. GloVe 路径不存在:no vectors found

① 当时的源码
vec=Vectors("glove.6B.100d.txt","./data")
TEXT.build_vocab(train_data,max_size=20000,vectors=vec)
LABEL.build_vocab(train_data)

print(TEXT.vocab.freqs.most_common(n=10))
print("词典的词数: ",len(TEXT.vocab.itos))
print("前10个单词:\n",TEXT.vocab.itos[0:10])

print("类别标签情况: ",LABEL.vocab.freqs)
② 报错信息
Traceback (most recent call last):
  File "/Users/tintin7790/Code/Python/2026_08_19_BasePT/TCPractise.py", line 155, in <module>
    vec=Vectors("glove.6B.100d.txt","./data")
  File "/Library/Frameworks/Python.framework/Versions/3.13/lib/python3.13/site-packages/torchtext/vocab.py", line 323, in __init__
    self.cache(name, cache, url=url, max_vectors=max_vectors)
    ~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Library/Frameworks/Python.framework/Versions/3.13/lib/python3.13/site-packages/torchtext/vocab.py", line 372, in cache
    raise RuntimeError('no vectors found at {}'.format(path))
RuntimeError: no vectors found at ./data/glove.6B.100d.txt

③ 错误原因与修正

这一句:

vec=Vectors("glove.6B.100d.txt","./data")

要求当前项目中真实存在:

./data/glove.6B.100d.txt

也就是:

2026_08_19_BasePT/
├── TCPractise.py
├── data/
│   └── glove.6B.100d.txt
└── TC_textdata/

当时没有下载 GloVe,因此 Vectors 无法找到文件,在 TEXT.build_vocab() 之前程序已经停止。

这里需要区分两个功能。

TEXT.build_vocab() 首先负责建立自己的词表:

训练文本
→ 统计单词频率
→ 最多选择 20000 种词
→ 建立 word <-> index

例如:

"<unk>" -> 0
"<pad>" -> 1
"movie" -> 2
"film" -> 3
...

如果额外提供:

vectors=vec

torchtext 会继续使用 GloVe 给词表中的词匹配预训练的 100 维向量。

因此有两种合法方案。

方案 A:当前实验不使用 GloVe

TEXT.build_vocab(
    train_data,
    max_size=20000
)

此时:

TEXT.vocab
├── stoi   string -> index
├── itos   index -> string
└── freqs  word -> frequency

后面的:

nn.Embedding(
    vocab_size,
    embedding_dim
)

会自己随机初始化 [vocab_size, embedding_dim] 的权重,并通过反向传播学习词向量。

因为:

LABEL=data.Field(
    sequential=False,
    use_vocab=False,
    ...
)

标签本身已经是 0 / 1,所以这一句可以省略:

LABEL.build_vocab(train_data)

查看类别数量可以直接:

from collections import Counter

print(
    Counter(
        ex.label
        for ex in train_data.examples
    )
)

方案 B:使用 GloVe

确保:

./data/glove.6B.100d.txt

真实存在,然后:

vec=Vectors(
    "glove.6B.100d.txt",
    "./data"
)

TEXT.build_vocab(
    train_data,
    max_size=20000,
    vectors=vec
)

此时除了 stoi / itos / freqs,还有:

TEXT.vocab.vectors

其 shape 类似:

[20002, 100]

表示:

约 20002 个 vocabulary token
×
每个 token 的 100 维预训练词向量

后面可以:

pretrained_embeddings=TEXT.vocab.vectors

model.embedding.weight.data.copy_(
    pretrained_embeddings
)

使用预训练向量初始化 nn.Embedding。

④ 正确输出

当前不加载 GloVe 时:

TEXT.build_vocab(
    train_data,
    max_size=20000
)

print(TEXT.vocab.freqs.most_common(n=10))
print("词典的词数: ",len(TEXT.vocab.itos))
print("前10个单词:\n",TEXT.vocab.itos[0:10])

输出形式:

[('movie', ...), ('film', ...), ('one', ...), ...]

词典的词数:  20002

前10个单词:
['<unk>', '<pad>', 'movie', 'film', ...]

其中具体词频和词的排序由当前训练集及文本预处理结果决定。

关系可以总结为:

TEXT
Field 对象
│
└── build_vocab(train_data)
        ↓
    TEXT.vocab
    Vocab 对象
    │
    ├── stoi
    │   "movie" -> 23
    │
    ├── itos
    │   23 -> "movie"
    │
    └── freqs
        "movie" -> 出现次数

使用 GloVe 时进一步增加:

TEXT.vocab.vectors
        ↓
[index, embedding_dimension]
        ↓
[20002, 100]

整个过程最终连接到:

CSV 中的 str
→ TabularDataset 中的 token
→ build_vocab 建立 word-ID 映射
→ BucketIterator 根据 vocab 将 token 转为 ID Tensor
→ nn.Embedding 将 word ID 转成词向量
→ TextCNN

5. TextCNN 完整实现与运行结果

本节完成 IMDB 影评数据从原始文本、文本预处理、torchtext 数据加载、词表建立、Embedding、TextCNN 网络构建,到训练和验证的完整流程。

1. 完整代码
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import os
import re
import string
import nltk

from nltk.corpus import stopwords
from nltk.tokenize import word_tokenize
from nltk.stem import PorterStemmer
import seaborn as sns
from wordcloud import WordCloud
import time 
import copy
import torch
from torch import nn
import torch.nn.functional as F
import torch.optim as optim
from torchvision import transforms
from torchtext import data
from torchtext.vocab import Vectors,GloVe
from sklearn.model_selection import train_test_split

#nltk.download("stopwords")
data_path="./IMDB_Dataset.csv"

data_Frame=pd.read_csv(data_path)
X=data_Frame["review"].values
print(X.shape)#(50000,)
print(type(X))#<class 'pandas.arrays.StringArray'>
#print(X)

y=data_Frame["sentiment"].map({
    "positive":1,
    "negative":0,
}).values

X_train, X_test, y_train, y_test = train_test_split(
    X,
    y,
    test_size=0.25,
    random_state=42,
    stratify=y
)

def text_preprocess(text_data):
    text_pre=[]
    for text1 in text_data:
        text1=re.sub("<br /><br />"," ",text1)
        text1=text1.lower()
        text1=re.sub("\d+","",text1)
        text1=text1.translate(
            str.maketrans(
                "",
                "",
                string.punctuation.replace("'","")
            )
        )
        text1=text1.strip()
        text_pre.append(text1)

    return np.array(text_pre)

train_text_pre1=text_preprocess(X_train)
test_text_pre1=text_preprocess(X_test)

def stop_stem_word(datalist,stop_words):
    datalist_pre=[]

    for text in datalist:
        text_words=word_tokenize(text)
        text_words=[w for w in text_words if not w in stop_words]

        #text_words=[
        #    w for w in text_words
        #    if len(re.findall("'",w))==0
        #]

        datalist_pre.append(text_words)

    return datalist_pre

stop_words=stopwords.words("english")
stop_words=set(stop_words)

train_text_pre2=stop_stem_word(
    train_text_pre1,
    stop_words
)

test_text_pre2=stop_stem_word(
    test_text_pre1,
    stop_words
)

#print(train_text_pre1[10000])
#print(train_text_pre2[10000])

texts=[" ".join(w) for w in train_text_pre2]

traindata_save=pd.DataFrame({
    "text":texts,
    "label":y_train,
})

#print(len(texts))
#print(len(y_train))
#print(traindata_save.head())

tests=[" ".join(w) for w in test_text_pre2]

testdata_save=pd.DataFrame({
    "text":tests,
    "label":y_test,
})

#traindata_save.to_csv(
#    "./TC_textdata/imdb_train.csv",
#    index=False,
#)

#testdata_save.to_csv(
#    "./TC_textdata/imdb_test.csv",
#    index=False,
#)

traindata=pd.DataFrame({
    "train_text":X_train,
    "train_word":train_text_pre2,
    "train_label":y_train
})

train_word_sum=[
    len(text)
    for text in train_text_pre2
]

#traindata["train_word_sum"]=train_word_sum
#plt.figure(figsize=(8,5))
#_=plt.hist(train_word_sum,bins=100)
#plt.xlabel("word number")
#plt.ylabel("Freq")
#plt.show()

mytokenize=lambda x:x.split()

TEXT=data.Field(
    sequential=True,
    tokenize=mytokenize,
    include_lengths=True,
    #让迭代器同时返回文本张量和每个句子的真实长度
    use_vocab=True,
    #基于训练数据构建词汇表,并把单词映射成数字索引
    batch_first=True,
    fix_length=200,
    #把所有句子填充或截断到固定长度200
)

LABEL=data.Field(
    sequential=False,
    use_vocab=False,
    pad_token=None,
    unk_token=None,
)

train_data,test_data=data.TabularDataset.splits(
    path="./TC_textdata",
    format="csv",
    train="imdb_train.csv",
    test="imdb_test.csv",
    fields=[
        ("text",TEXT),
        ("label",LABEL)
    ],
    skip_header=True,
)

print(len(train_data),len(test_data))

ex0=train_data.examples[2]

#print(ex0.label)
#print(ex0.text)

train_data,val_data=train_data.split(
    split_ratio=0.7
)

print(
    len(train_data),
    len(val_data)
)

#vec=Vectors(
#    "glove.6B.100d.txt",
#    "./data"
#)

TEXT.build_vocab(
    train_data,
    max_size=20000
)

LABEL.build_vocab(train_data)

print(
    TEXT.vocab.freqs.most_common(n=10)
)

print(
    "词典的词数: ",
    len(TEXT.vocab.itos)
)

print(
    "前10个单词:\n",
    TEXT.vocab.itos[0:10]
)

print(
    "类别标签情况: ",
    LABEL.vocab.freqs
)

BATCH_SIZE=32

train_iter=data.BucketIterator(
    train_data,
    batch_size=BATCH_SIZE
)

val_iter=data.BucketIterator(
    val_data,
    batch_size=BATCH_SIZE
)

test_iter=data.BucketIterator(
    test_data,
    batch_size=BATCH_SIZE
)

for step,batch in enumerate(train_iter):
    if step>0:
        break

print(
    "type batch: ",
    type(batch)
)

print(
    "type batch.label: ",
    type(batch.label)
)

print(
    "type batch.text[0]: ",
    type(batch.text[0])
)

print(
    "type batch.text[1]: ",
    type(batch.text[1])
)

print(
    "数据类别标签: \n",
    batch.label
)

print(
    "数据尺寸: ",
    batch.text[0].shape
)

print(
    "数据样本数目: ",
    len(batch.text[1])
)


class CNN_Text(nn.Module):

    def __init__(
        self,
        vocab_size,
        embedding_dim,
        n_filters,
        filter_sizes,
        output_dim,
        dropout,
        pad_idx
    ):
        super().__init__()

        self.embedding=nn.Embedding(
            vocab_size,
            embedding_dim,
            padding_idx=pad_idx
        )

        self.convs=nn.ModuleList([
            nn.Conv2d(
                in_channels=1,
                out_channels=n_filters,
                kernel_size=(fs,embedding_dim)
            )
            for fs in filter_sizes
        ])

        self.fc=nn.Linear(
            len(filter_sizes)*n_filters,
            output_dim
        )

        self.dropout=nn.Dropout(dropout)


    def forward(self,text):

        # text=[batch size,sent len]

        embedded=self.embedding(text)

        # embedded=[batch size,sent len,emb dim]

        embedded=embedded.unsqueeze(1)

        # embedded=[batch size,1,sent len,emb dim]

        conved=[
            F.relu(
                conv(embedded)
            ).squeeze(3)

            for conv in self.convs
        ]

        # conved_n=[
        # batch size,
        # n_filters,
        # sent len-filter_sizes[n]+1
        # ]

        pooled=[
            F.max_pool1d(
                conv,
                conv.shape[2]
            ).squeeze(2)

            for conv in conved
        ]

        # pooled_n=[batch size,n_filters]

        cat=self.dropout(
            torch.cat(
                pooled,
                dim=1
            )
        )

        # cat=[
        # batch size,
        # n_filters*len(filter_size)
        # ]

        return self.fc(cat)


INPUT_DIM=len(TEXT.vocab)
EMBEDDING_DIM=100
N_FILTERS=100
FILTER_SIZE=[3,4,5]
OUTPUT_DIM=1
DROPOUT=0.5

PAD_IDX=TEXT.vocab.stoi[
    TEXT.pad_token
]

model=CNN_Text(
    INPUT_DIM,
    EMBEDDING_DIM,
    N_FILTERS,
    FILTER_SIZE,
    OUTPUT_DIM,
    DROPOUT,
    PAD_IDX
)

print(model)

#pretrained_embeddings=TEXT.vocab.vectors

UNK_IDX=TEXT.vocab.stoi[
    TEXT.unk_token
]

model.embedding.weight.data[
    UNK_IDX
]=torch.zeros(
    EMBEDDING_DIM
)

model.embedding.weight.data[
    PAD_IDX
]=torch.zeros(
    EMBEDDING_DIM
)

optimizer=optim.Adam(
    model.parameters()
)

criterion=nn.BCEWithLogitsLoss()


def train_epoch(
    model,
    iterator,
    optimizer,
    criterion
):

    epoch_loss=0
    train_acc=0
    train_corrects=0
    train_num=0

    model.train()

    for batch in iterator:

        optimizer.zero_grad()

        pre=model(
            batch.text[0]
        ).squeeze(1)

        loss=criterion(
            pre,
            batch.label.type(
                torch.FloatTensor
            )
        )

        pre_lab=torch.round(
            torch.sigmoid(pre)
        )

        train_corrects+=torch.sum(
            pre_lab.long()==batch.label
        )

        train_num+=len(
            batch.label
        )

        loss.backward()

        optimizer.step()

        epoch_loss+=loss.item()

    epoch_loss=epoch_loss/train_num

    epoch_acc=(
        train_corrects
        .double()
        .item()
        /train_num
    )

    return epoch_loss,epoch_acc


def evaluate(
    model,
    iterator,
    criterion
):

    epoch_loss=0
    train_acc=0
    train_corrects=0
    train_num=0

    model.eval()

    with torch.no_grad():

        for batch in iterator:

            pre=model(
                batch.text[0]
            ).squeeze(1)

            loss=criterion(
                pre,
                batch.label.type(
                    torch.FloatTensor
                )
            )

            pre_lab=torch.round(
                torch.sigmoid(pre)
            )

            train_corrects+=torch.sum(
                pre_lab.long()==batch.label
            )

            train_num+=len(
                batch.label
            )

            epoch_loss+=loss.item()

    epoch_loss=epoch_loss/train_num

    epoch_acc=(
        train_corrects
        .double()
        .item()
        /train_num
    )

    return epoch_loss,epoch_acc


EPOCHS=10

best_val_loss=float("inf")
# 初始化一个“最大值”变量(找最小值)

best_acc=float(0)

for epoch in range(EPOCHS):

    start_time=time.time()

    train_loss,train_acc=train_epoch(
        model,
        train_iter,
        optimizer,
        criterion
    )

    val_loss,val_acc=evaluate(
        model,
        val_iter,
        criterion
    )

    end_time=time.time()

    print(
        "Epoch: ",
        epoch+1,
        "|",
        "Epoch Time: ",
        end_time-start_time,
        "s"
    )

    print(
        "Train Loss: ",
        train_loss,
        "|",
        "Train Acc: ",
        train_acc
    )

    print(
        "Val Loss: ",
        val_loss,
        "|",
        "Val Acc: ",
        val_acc
    )

    if (
        val_loss<best_val_loss
        and
        val_acc>best_acc
    ):

        best_model_wts=copy.deepcopy(
            model.state_dict()
        )

        best_val_loss=val_loss
        best_acc=val_acc

model.load_state_dict(
    best_model_wts
)
2. 完整运行输出
(50000,)
<class 'pandas.arrays.StringArray'>

37500 12500
26250 11250

[("'s", 64109),
 ('movie', 44760),
 ('film', 40995),
 ("n't", 34845),
 ('one', 27234),
 ('like', 20373),
 ("'", 17508),
 ('good', 15139),
 ('would', 13907),
 ('even', 12827)]

词典的词数:  20002

前10个单词:
['<unk>', '<pad>', "'s", 'movie', 'film', "n't", 'one', 'like', "'", 'good']

类别标签情况:
Counter({'1': 13153, '0': 13097})

type batch:
<class 'torchtext.data.batch.Batch'>

type batch.label:
<class 'torch.Tensor'>

type batch.text[0]:
<class 'torch.Tensor'>

type batch.text[1]:
<class 'torch.Tensor'>

数据类别标签:
tensor([0, 0, 1, 1, 0, 1, 0, 1,
        1, 0, 0, 1, 1, 0, 0, 0,
        1, 1, 1, 1, 0, 0, 1, 0,
        0, 0, 1, 0, 1, 0, 0, 1])

数据尺寸:
torch.Size([32, 200])

数据样本数目:
32

CNN_Text(
  (embedding): Embedding(20002, 100, padding_idx=1)

  (convs): ModuleList(
    (0): Conv2d(
        1,
        100,
        kernel_size=(3, 100),
        stride=(1, 1)
    )

    (1): Conv2d(
        1,
        100,
        kernel_size=(4, 100),
        stride=(1, 1)
    )

    (2): Conv2d(
        1,
        100,
        kernel_size=(5, 100),
        stride=(1, 1)
    )
  )

  (fc):
  Linear(
      in_features=300,
      out_features=1,
      bias=True
  )

  (dropout):
  Dropout(
      p=0.5,
      inplace=False
  )
)

Epoch:  1 | Epoch Time:  22.937171936035156 s
Train Loss:  0.017959075016067142 | Train Acc:  0.6922666666666667
Val Loss:  0.013360962017377218 | Val Acc:  0.7967111111111111

Epoch:  2 | Epoch Time:  22.564228057861328 s
Train Loss:  0.013155158798467546 | Train Acc:  0.807847619047619
Val Loss:  0.011004582006401486 | Val Acc:  0.8436444444444444

Epoch:  3 | Epoch Time:  23.735124826431274 s
Train Loss:  0.01052334681777727 | Train Acc:  0.8558095238095238
Val Loss:  0.010290144343508615 | Val Acc:  0.8584

Epoch:  4 | Epoch Time:  22.851189136505127 s
Train Loss:  0.008800397040588515 | Train Acc:  0.884
Val Loss:  0.009483525474203957 | Val Acc:  0.8698666666666667

Epoch:  5 | Epoch Time:  23.82130479812622 s
Train Loss:  0.007318048346042633 | Train Acc:  0.9042285714285714
Val Loss:  0.009488074294726053 | Val Acc:  0.8744

Epoch:  6 | Epoch Time:  23.11823272705078 s
Train Loss:  0.006165434577351525 | Train Acc:  0.92
Val Loss:  0.01106443339255121 | Val Acc:  0.8660444444444444

Epoch:  7 | Epoch Time:  23.370702028274536 s
Train Loss:  0.005004313456941218 | Train Acc:  0.9362285714285714
Val Loss:  0.010834257153669993 | Val Acc:  0.8688

Epoch:  8 | Epoch Time:  22.620039224624634 s
Train Loss:  0.00397009183210986 | Train Acc:  0.9514666666666667
Val Loss:  0.011867772964305348 | Val Acc:  0.8698666666666667

Epoch:  9 | Epoch Time:  23.58990502357483 s
Train Loss:  0.0033183299140383798 | Train Acc:  0.9591238095238095
Val Loss:  0.013353233994709121 | Val Acc:  0.8661333333333333

Epoch:  10 | Epoch Time:  23.85074496269226 s
Train Loss:  0.002677837031547512 | Train Acc:  0.9684190476190476
Val Loss:  0.014593654695153236 | Val Acc:  0.8618666666666667

TextCNN 文本情感分类完整数据流程

本项目使用 IMDB 影评数据完成二分类情感分析。

整体数据流:

原始 CSV
→ pandas.DataFrame
→ 提取 review / sentiment
→ train_test_split
→ 文本清洗
→ 分词 + 去停用词
→ 保存处理后的 CSV
→ Field 定义处理规则
→ TabularDataset 读取数据
→ build_vocab 建立词表
→ BucketIterator 生成 Batch
→ 单词 ID Tensor
→ Embedding 词向量
→ TextCNN
→ BCEWithLogitsLoss
→ Adam
→ 训练 / 验证
→ 保存最佳模型参数

1. 读取原始 IMDB 数据

data_Frame = pd.read_csv("./IMDB_Dataset.csv")

pd.read_csv() 返回:

type(data_Frame)
# pandas.DataFrame

原始数据:

review                                  sentiment
"This movie is great..."                positive
"This movie is terrible..."             negative
...

提取影评:

X = data_Frame["review"].values

数据结构:

X.shape = (50000,)

X
├── X[0] -> 一整条影评字符串
├── X[1] -> 一整条影评字符串
└── ...

这里 (50000,) 表示:

50000 个元素
每个元素是一整条影评
不是 50000 个字符

提取标签:

y = data_Frame["sentiment"].map({
    "positive": 1,
    "negative": 0
}).values

于是:

positive -> 1
negative -> 0

并且:

X[i] <-> y[i]

始终表示同一条数据。


2. 划分训练集和测试集

X_train, X_test, y_train, y_test = train_test_split(
    X,
    y,
    test_size=0.25,
    random_state=42,
    stratify=y
)

数据数量:

原始:50000

├── train:37500
│   ├── X_train
│   └── y_train
│
└── test:12500
    ├── X_test
    └── y_test

其中:

test_size=0.25

表示 25% 作为测试集。

stratify=y

表示尽量保持训练集和测试集中:

positive / negative

的比例一致。


3. 第一阶段文本预处理

函数:

def text_preprocess(text_data):
    text_pre = []

    for text1 in text_data:

        text1 = re.sub("<br /><br />", " ", text1)

        text1 = text1.lower()

        text1 = re.sub(r"\d+", "", text1)

        text1 = text1.translate(
            str.maketrans(
                "",
                "",
                string.punctuation.replace("'", "")
            )
        )

        text1 = text1.strip()

        text_pre.append(text1)

    return np.array(text_pre)

主要完成:

原始字符串
↓
删除 <br /><br />
↓
lower() 转小写
↓
删除数字
↓
删除大部分标点
↓
strip() 删除首尾空白

例如:

"I LOVE this movie!! <br /><br /> 10/10"

处理后类似:

"i love this movie"

调用:

train_text_pre1 = text_preprocess(X_train)
test_text_pre1 = text_preprocess(X_test)

此时:

X_train
│
│ 每个元素:原始 str
↓
text_preprocess()
↓
train_text_pre1
│
│ numpy.ndarray
│
└── 每个元素仍然是一整条 str

也就是说:

这一步只是清洗字符串
还没有变成单词列表
更没有变成 Tensor

4. 第二阶段:分词 + 去停用词

函数:

def stop_stem_word(datalist, stop_words):

    datalist_pre = []

    for text in datalist:

        text_words = word_tokenize(text)

        text_words = [
            w for w in text_words
            if w not in stop_words
        ]

        datalist_pre.append(text_words)

    return datalist_pre

准备英文停用词:

stop_words = stopwords.words("english")
stop_words = set(stop_words)

其中:

stopwords.words("english")

得到英文停用词。

例如:

the
a
an
is
are
...

转换成:

set

主要是方便快速进行:

word in stop_words

查询。

调用:

train_text_pre2 = stop_stem_word(
    train_text_pre1,
    stop_words
)

test_text_pre2 = stop_stem_word(
    test_text_pre1,
    stop_words
)

这里发生了重要的数据类型变化:

train_text_pre1

numpy.ndarray
│
├── "this movie is really good"
├── "this film is terrible"
└── ...
        ↓
    word_tokenize
        ↓
train_text_pre2

Python list
│
├── ["movie", "really", "good"]
├── ["film", "terrible"]
└── ...

所以:

type(train_text_pre2)
# list

type(train_text_pre2[0])
# list

type(train_text_pre2[0][0])
# str

注意:

函数名虽然叫:

stop_stem_word

但当前代码实际上没有执行 stemming。


5. 单词列表重新拼成字符串并保存 CSV

现在:

train_text_pre2[0]

类似:

["movie", "really", "good"]

执行:

texts = [
    " ".join(w)
    for w in train_text_pre2
]

得到:

["movie really good",
 "film terrible",
 ...]

也就是:

list[str]

然后:

traindata_save = pd.DataFrame({
    "text": texts,
    "label": y_train
})

得到:

text                        label
movie really good             1
film terrible                 0
...

测试集同理:

tests = [
    " ".join(w)
    for w in test_text_pre2
]

testdata_save = pd.DataFrame({
    "text": tests,
    "label": y_test
})

最后可以保存:

traindata_save.to_csv(
    "./TC_textdata/imdb_train.csv",
    index=False
)

testdata_save.to_csv(
    "./TC_textdata/imdb_test.csv",
    index=False
)

6. Field:定义 torchtext 如何处理文本

首先定义 tokenize:

mytokenize = lambda x: x.split()

例如:

"movie really good"
↓
["movie", "really", "good"]

然后:

TEXT = data.Field(
    sequential=True,
    tokenize=mytokenize,
    include_lengths=True,
    use_vocab=True,
    batch_first=True,
    fix_length=200
)

TEXT 是:

torchtext.data.Field 对象

它:

不是 Tensor
不是 Dataset
也不是词表

它是一个:

“文本应该如何处理”的规则对象

保存了这些规则:

sequential=True
→ text 是序列数据

tokenize=mytokenize
→ 使用 x.split() 分词

include_lengths=True
→ batch 中同时记录句子的真实长度

use_vocab=True
→ 使用词表将 word 转换成 ID

batch_first=True
→ Tensor 使用 [batch, sequence]

fix_length=200
→ 每条影评统一为 200 个 token

其中:

不足 200
→ padding

超过 200
→ truncate 截断

标签:

LABEL = data.Field(
    sequential=False,
    use_vocab=False,
    pad_token=None,
    unk_token=None
)

因为标签只是:

0 / 1

不是序列,也不需要词表。


7. TabularDataset:按照 Field 规则读取 CSV

train_data, test_data = data.TabularDataset.splits(
    path="./TC_textdata",
    format="csv",

    train="imdb_train.csv",
    test="imdb_test.csv",

    fields=[
        ("text", TEXT),
        ("label", LABEL)
    ],

    skip_header=True
)

这里建立的是:

train_data
test_data

它们是 torchtext 的 Dataset 对象。

关键关系:

CSV 中的 text
→ 使用 TEXT 规则

CSV 中的 label
→ 使用 LABEL 规则

所以:

fields=[
    ("text", TEXT),
    ("label", LABEL)
]

相当于告诉 torchtext:

text 这一列是谁负责?
→ TEXT

label 这一列是谁负责?
→ LABEL

8. Example:Dataset 中的一条样本

ex0 = train_data.examples[2]

train_data.examples:

保存 Dataset 中所有样本

所以:

train_data.examples[2]

就是:

第 3 条样本

可以访问:

ex0.text
ex0.label

例如:

ex0.text

此时仍然是:

[
    "movie",
    "journey",
    "mind",
    "screenwriter",
    ...
]

注意:

这里仍然不是 Tensor

目前还是:

word 的字符串 list

9. 再划分 train / validation

train_data, val_data = train_data.split(
    split_ratio=0.7
)

原来的训练数据再次划分:

37500
│
├── train_data ≈ 26250
└── val_data   ≈ 11250

最终得到:

train
validation
test

三个数据集。

用途:

train
→ 用来更新模型参数

validation
→ 训练过程中判断模型效果

test
→ 最终测试

10. build_vocab:建立词表

TEXT.build_vocab(
    train_data,
    max_size=20000
)

注意:

build_vocab() 不会把 train_data 变成 Tensor

train_data 里面仍然保存字符串 token。

build_vocab() 做的是:

train_data 中所有单词
↓
统计词频
↓
选择最多 20000 种词
↓
建立 Vocab 对象
↓
保存到 TEXT.vocab

对象关系:

TEXT
│
│ Field 对象
│
└── vocab
     │
     │ Vocab 对象
     │
     ├── stoi
     │   string -> index
     │
     ├── itos
     │   index -> string
     │
     └── freqs
         单词 -> 出现频率

11. stoi:字符串 -> ID

TEXT.vocab.stoi

可以理解成一个映射表:

"<unk>" -> 0
"<pad>" -> 1
"movie" -> 2
"film"  -> 3
"good"  -> 4
...

所以:

TEXT.vocab.stoi["movie"]

可能得到:

2

stoi 的含义:

string to index

它不是函数。

它类似一个字典,所以使用:

stoi["movie"]

而不是:

stoi("movie")

12. itos:ID -> 字符串

TEXT.vocab.itos

是反向映射:

0 -> "<unk>"
1 -> "<pad>"
2 -> "movie"
3 -> "film"
...

所以:

TEXT.vocab.itos[2]

可能得到:

"movie"

itos:

index to string

13. 特殊 token:UNK 和 PAD

TEXT.unk_token

通常:

"<unk>"

表示:

unknown word

训练词表中没有出现的词会被映射到 <unk>。

例如:

UNK_IDX = TEXT.vocab.stoi[
    TEXT.unk_token
]

等价于:

UNK_IDX = TEXT.vocab.stoi["<unk>"]

通常可能:

UNK_IDX = 0

同理:

PAD_IDX = TEXT.vocab.stoi[
    TEXT.pad_token
]

等价于:

PAD_IDX = TEXT.vocab.stoi["<pad>"]

可能:

PAD_IDX = 1

14. BucketIterator:真正生成训练 Batch

BATCH_SIZE = 32

train_iter = data.BucketIterator(
    train_data,
    batch_size=BATCH_SIZE
)

val_iter = data.BucketIterator(
    val_data,
    batch_size=BATCH_SIZE
)

test_iter = data.BucketIterator(
    test_data,
    batch_size=BATCH_SIZE
)

这里非常重要:

Dataset
≠
DataLoader / Iterator

关系:

train_data
Dataset
保存所有样本
      ↓
BucketIterator
      ↓
train_iter
负责一批一批取数据

执行:

for batch in train_iter:
    ...

此时:

type(batch)

为:

torchtext.data.batch.Batch

一个 batch 中有:

batch
│
├── batch.text
│
└── batch.label

15. 字符串真正在哪里变成 Tensor?

不是:

TEXT.build_vocab()

直接修改 Dataset。

真正取 batch 时,BucketIterator 会根据:

TEXT
+
TEXT.vocab

完成:

字符串 token
↓
stoi
↓
word ID
↓
padding / truncate
↓
组成 Tensor

例如:

["movie", "really", "good"]

↓ stoi

[2, 53, 91]

↓ fix_length=200

[2, 53, 91, 1, 1, 1, ..., 1]

↓ 32条组成 batch

Tensor [32, 200]

因此:

batch.text[0].shape

得到:

torch.Size([32, 200])

含义:

32
→ 一个 batch 有 32 条影评

200
→ 每条影评统一为 200 个 word ID

这里的 200 个数字:

不是词向量

目前仍然只是:

word ID

16. batch.text 为什么有 [0] 和 [1]

因为:

TEXT = data.Field(
    include_lengths=True,
    ...
)

所以:

batch.text

里面包含:

batch.text[0]
→ 文本 Tensor

batch.text[1]
→ 每条文本的真实长度

其中:

batch.text[0].shape
# [32, 200]

而:

len(batch.text[1])
# 32

因为一个 batch 有 32 条影评,所以记录 32 个真实长度。


17. Embedding:word ID -> 词向量

模型:

self.embedding = nn.Embedding(
    vocab_size,
    embedding_dim,
    padding_idx=pad_idx
)

这里:

vocab_size = 20002
embedding_dim = 100

所以:

model.embedding.weight.shape

为:

[20002, 100]

可以理解成一张表:

word ID          100维词向量

0 <unk>   -> [................]
1 <pad>   -> [................]
2 movie   -> [................]
3 film    -> [................]
...

Embedding 的核心作用:

word ID
↓
查表
↓
word vector

所以输入:

[batch, sent_len]

[32, 200]

经过:

embedded = self.embedding(text)

得到:

[batch, sent_len, embedding_dim]

[32, 200, 100]

也就是:

32 条影评
×
200 个词
×
每个词 100 维向量

如果没有使用 GloVe:

Embedding 权重
→ PyTorch 随机初始化
→ 训练过程中通过反向传播学习

如果使用 GloVe:

Embedding 权重
→ 使用预训练词向量初始化
→ 再继续训练

18. padding_idx 的作用

nn.Embedding(
    vocab_size,
    embedding_dim,
    padding_idx=PAD_IDX
)

注意:

padding_idx 不负责添加 padding

真正添加 padding 的是前面的文本处理流程。

padding_idx 的作用是告诉 Embedding:

这个 ID 是 <pad>
它不是真正的单词

例如:

PAD_IDX = 1

表示:

word ID = 1
→ <pad>

19. 将 UNK 和 PAD 的词向量设为 0

UNK_IDX = TEXT.vocab.stoi[
    TEXT.unk_token
]

找到:

<unk> 对应的 ID

然后:

model.embedding.weight.data[UNK_IDX] = \
    torch.zeros(EMBEDDING_DIM)

表示:

<unk>
→ [0, 0, 0, ..., 0]

同理:

model.embedding.weight.data[PAD_IDX] = \
    torch.zeros(EMBEDDING_DIM)

表示:

<pad>
→ [0, 0, 0, ..., 0]

20. TextCNN 网络结构

模型:

class CNN_Text(nn.Module):

    def __init__(
        self,
        vocab_size,
        embedding_dim,
        n_filters,
        filter_sizes,
        output_dim,
        dropout,
        pad_idx
    ):

        super().__init__()

        self.embedding = nn.Embedding(
            vocab_size,
            embedding_dim,
            padding_idx=pad_idx
        )

        self.convs = nn.ModuleList([
            nn.Conv2d(
                in_channels=1,
                out_channels=n_filters,
                kernel_size=(fs, embedding_dim)
            )
            for fs in filter_sizes
        ])

        self.fc = nn.Linear(
            len(filter_sizes) * n_filters,
            output_dim
        )

        self.dropout = nn.Dropout(dropout)

参数:

vocab_size     = 20002
embedding_dim  = 100
n_filters      = 100
filter_sizes   = [3,4,5]
output_dim     = 1
dropout        = 0.5

21. ModuleList:保存多个卷积分支

self.convs = nn.ModuleList([
    nn.Conv2d(
        1,
        100,
        kernel_size=(fs, 100)
    )
    for fs in [3,4,5]
])

得到三个卷积层:

Conv2d kernel = 3×100
Conv2d kernel = 4×100
Conv2d kernel = 5×100

ModuleList:

是 PyTorch Module 的容器

它负责正确注册这些卷积层。

逻辑上可以把三个卷积理解成三个并列分支:

                    Conv(3×100)
                   /
Embedding Tensor -- Conv(4×100)
                   \
                    Conv(5×100)

22. forward:Embedding

输入:

text

shape:

[32, 200]

执行:

embedded = self.embedding(text)

得到:

[32, 200, 100]

再:

embedded = embedded.unsqueeze(1)

增加 channel 维:

[32, 200, 100]
↓
[32, 1, 200, 100]

对应 Conv2d:

[batch, channel, height, width]

这里:

batch   = 32
channel = 1
height  = 200 个词
width   = 100 维词向量

23. 三种卷积核提取不同长度的局部特征

conved = [
    F.relu(
        conv(embedded)
    ).squeeze(3)

    for conv in self.convs
]

列表推导式等价于:

conved = []

for conv in self.convs:

    x = conv(embedded)

    x = F.relu(x)

    x = x.squeeze(3)

    conved.append(x)

三个卷积分支:

输入:
[32,1,200,100]

3×100 kernel
↓
[32,100,198]

4×100 kernel
↓
[32,100,197]

5×100 kernel
↓
[32,100,196]

这里:

100
→ n_filters
→ 每种卷积核有 100 个输出 channel

24. Max Pooling

pooled = [
    F.max_pool1d(
        conv,
        conv.shape[2]
    ).squeeze(2)

    for conv in conved
]

例如:

[32,100,198]

在最后的 198 个位置中取最大值:

[32,100,198]
↓ max pooling
[32,100,1]
↓ squeeze
[32,100]

所以三个卷积分支最终:

3×100 -> [32,100]
4×100 -> [32,100]
5×100 -> [32,100]

25. 拼接三个卷积分支

torch.cat(
    pooled,
    dim=1
)

三个:

[32,100]
[32,100]
[32,100]

沿:

dim=1

也就是 feature 维拼接:

[32,100]
+
[32,100]
+
[32,100]

↓

[32,300]

可以理解成:

3-word features | 4-word features | 5-word features
     100        |       100       |       100

↓

300维特征

26. Dropout

cat = self.dropout(
    torch.cat(pooled, dim=1)
)

shape 不变:

[32,300]
→
[32,300]

训练过程中随机屏蔽部分特征,用于降低过拟合。

这里 Dropout 放在:

CNN
↓
MaxPool
↓
Concat
↓
Dropout
↓
Linear

目的是避免最终分类器过度依赖某几个卷积特征。


27. Linear 输出分类结果

self.fc = nn.Linear(
    300,
    1
)

因此:

[32,300]
↓ Linear
[32,1]

最终:

return self.fc(cat)

输出的是:

logits

而不是已经经过 sigmoid 的概率。


28. TextCNN 完整 shape 变化

batch.text[0]

[32,200]
32条影评 × 200个word ID

        ↓ Embedding

[32,200,100]
每个word ID → 100维词向量

        ↓ unsqueeze

[32,1,200,100]

        ↓

┌────────────────┬────────────────┬────────────────┐
│                │                │                │
│ Conv 3×100     │ Conv 4×100     │ Conv 5×100     │
│                │                │                │
│ [32,100,198]   │ [32,100,197]   │ [32,100,196]   │
│                │                │                │
│ MaxPool        │ MaxPool        │ MaxPool        │
│                │                │                │
│ [32,100]       │ [32,100]       │ [32,100]       │
│                │                │                │
└────────────────┴────────────────┴────────────────┘

                 ↓ cat(dim=1)

                   [32,300]

                 ↓ Dropout

                   [32,300]

                 ↓ Linear

                    [32,1]

                 ↓ squeeze(1)

                     [32]

29. Adam 优化器

optimizer = optim.Adam(
    model.parameters()
)

model.parameters() 包含模型需要学习的参数:

Embedding.weight
Conv.weight
Conv.bias
Linear.weight
Linear.bias

Adam 根据:

gradient

更新这些参数。

基本训练顺序:

optimizer.zero_grad()

prediction = model(x)

loss = criterion(prediction, y)

loss.backward()

optimizer.step()

即:

清梯度
→ forward
→ loss
→ backward
→ Adam 更新 weight / bias

30. BCEWithLogitsLoss

criterion = nn.BCEWithLogitsLoss()

用于:

二分类

模型直接输出:

logit

因此模型内部不需要:

Sigmoid()

因为:

BCEWithLogitsLoss
≈
Sigmoid + Binary Cross Entropy

已经组合在一起。


31. label 为什么需要 float

原始:

batch.label

类似:

tensor([1,0,1,0,...])

通常是整数。

但是:

BCEWithLogitsLoss

需要浮点 target。

所以:

batch.label.float()

得到:

tensor([1.,0.,1.,0.,...])

教材写法:

batch.label.type(torch.FloatTensor)

本质也是转换为浮点 Tensor。


32. sigmoid + round 得到预测标签

模型输出:

logit

[-2.1, 1.7, 0.3, -0.8]

执行:

torch.sigmoid(pre)

得到 0~1:

[0.109, 0.846, 0.574, 0.310]

再:

torch.round(...)

得到:

[0,1,1,0]

所以:

pre_lab = torch.round(
    torch.sigmoid(pre)
)

用于把模型输出转换成最终二分类结果。


33. long()

pre_lab.long()

把:

tensor([0.,1.,1.,0.])

转换为:

tensor([0,1,1,0])

即:

torch.int64

方便与:

batch.label

比较:

pre_lab.long() == batch.label

得到:

True
True
False
True
...

再:

torch.sum(...)

计算当前 batch 预测正确的样本数量。


34. train_num 统计的是样本数

train_num += len(batch.label)

一个 batch:

batch.label.shape = [32]

所以:

len(batch.label)
# 32

每次:

train_num += 32

统计的是:

已经处理多少条影评

而不是:

训练了多少个 batch

35. loss.item()

loss

是一个标量 Tensor,例如:

tensor(0.6832, grad_fn=...)

执行:

loss.item()

得到普通 Python 数字:

0.6832

注意:

.item()
不是求和

只是:

标量 Tensor
→ Python number

36. model.train() 和 model.eval()

训练:

model.train()

告诉模型:

当前处于训练模式

此时:

Dropout 开启

验证:

model.eval()

告诉模型:

当前处于推理 / 验证模式

此时:

Dropout 关闭

37. torch.no_grad()

验证时:

with torch.no_grad():

表示:

不记录梯度
不建立反向传播所需计算图

因为 validation:

只需要 forward
不需要 backward
不需要 optimizer.step()

可以减少内存和计算开销。


38. 保存最佳模型

初始化:

best_val_loss = float("inf")
best_acc = 0.0

其中:

float("inf")

表示:

正无穷

方便寻找最小 validation loss。

训练过程中:

if val_loss < best_val_loss:

    best_model_wts = copy.deepcopy(
        model.state_dict()
    )

    best_val_loss = val_loss

其中:

model.state_dict()

保存模型的参数状态:

Embedding weight
Conv weight / bias
Linear weight / bias
...

copy.deepcopy():

复制当前模型参数

避免后面的训练继续修改这份最佳参数。

最后:

model.load_state_dict(
    best_model_wts
)

把最佳参数重新加载到模型。


39. 本项目中的核心对象关系

data_Frame
│
│ pandas.DataFrame
│
↓
X / y
│
│ array-like
│
↓
X_train / X_test
│
↓
text_preprocess()
│
↓
train_text_pre1
│
│ numpy.ndarray
│ 每个元素 = 清洗后的 str
│
↓
stop_stem_word()
│
↓
train_text_pre2
│
│ Python list
│ 每个元素 = list[str]
│
↓
" ".join()
│
↓
texts
│
│ list[str]
│
↓
pd.DataFrame()
│
↓
imdb_train.csv
│
↓
TabularDataset
│
↓
train_data
│
│ torchtext Dataset
│
│ 每个 Example 中仍然是 token 字符串
│
├──────────────→ TEXT.build_vocab(train_data)
│                       │
│                       ↓
│                   TEXT.vocab
│                       │
│                       ├── stoi
│                       ├── itos
│                       └── freqs
│
↓
BucketIterator
│
↓
train_iter
│
↓
batch
│
│ torchtext.data.Batch
│
├── batch.text[0]
│      │
│      │ torch.Tensor [32,200]
│      │ word ID
│      ↓
│   nn.Embedding
│      ↓
│   Tensor [32,200,100]
│      ↓
│   TextCNN
│      ↓
│   logits [32]
│
└── batch.label
       │
       │ Tensor [32]
       ↓
     target

40. TEXT / vocab / stoi / itos 的关系

TEXT
Field 对象
│
├── tokenize
├── fix_length
├── batch_first
├── pad_token
├── unk_token
│
└── vocab
     Vocab 对象
     │
     ├── stoi
     │   string -> index
     │
     │   "movie" -> 23
     │   "good"  -> 51
     │
     │
     ├── itos
     │   index -> string
     │
     │   23 -> "movie"
     │   51 -> "good"
     │
     └── freqs
         word -> frequency

41. Dataset / Iterator / Batch 的关系

train_data
Dataset
保存所有训练样本
        ↓

train_iter
BucketIterator
负责分 batch
        ↓

batch
Batch 对象
保存当前这一批数据
        ↓

batch.text[0]
Tensor [32,200]

batch.label
Tensor [32]

可以类比普通 PyTorch:

TensorDataset
    ↓
DataLoader
    ↓
batch

torchtext 这里:

TabularDataset
    ↓
BucketIterator
    ↓
Batch

42. 最终最重要的数据类型变化

原始 CSV

"This movie is really good!"
str
        ↓
文本清洗

"this movie is really good"
str
        ↓
tokenize

["movie", "really", "good"]
list[str]
        ↓
stoi

[23, 57, 81]
word ID
        ↓
padding / truncate

[23,57,81,1,1,1,...]
长度 200
        ↓
BucketIterator

torch.Tensor
[batch,200]
        ↓
Embedding

torch.Tensor
[batch,200,100]
        ↓
TextCNN

torch.Tensor
[batch,1]
        ↓
squeeze

torch.Tensor
[batch]
        ↓
sigmoid

0~1
        ↓
round

0 / 1
        ↓
与真实 label 比较
        ↓
accuracy

43. 一句话总结

整个 TextCNN 项目最核心的数据变化可以记成:

文本字符串
→ token 单词列表
→ vocab 建立 word↔ID 映射
→ Iterator 根据 vocab 转成 ID Tensor
→ Embedding 将每个 ID 转成可学习词向量
→ CNN 提取 3/4/5-gram 局部特征
→ MaxPool 保留最强特征
→ 拼接特征
→ Dropout
→ Linear 输出 logit
→ BCEWithLogitsLoss 计算二分类损失
→ backward 计算梯度
→ Adam 更新 Embedding / CNN / Linear 参数
→ validation 选择最佳模型

posted on 2026-08-22 23:36  tintin7790  阅读(12)  评论(0)    收藏  举报

刷新页面返回顶部
 
博客园  ©  2004-2026
浙公网安备 33010602011771号 浙ICP备2021040463号-3