0
点赞
收藏
分享

微信扫一扫

NLP项目:电影评论情感分类

ZMXQQ233 05-06 12:00 阅读 8

源数据获取

as_supervised 参数是一个布尔值,当设置为 True 时,tfds.load 函数会将数据集以监督学习的格式返回,即每个样本包含输入数据(电影评论)和对应的标签(情感标签)。 这样返回的 train_data 和 test_data 中的每个元素都是一个二元组 (input, label),便于后续的模型训练。 data_dir:参数是可选的,用于指定数据集的存储路径。如果指定了该路径,tfds.load 函数会在该路径下查找数据集文件。 如果数据集文件不存在,它会自动从互联网下载并保存到指定路径。这样可以避免每次运行代码都重新下载数据集。

# import tensorflow_datasets as tfds

# # 加载数据集
# train_data, validation_data, test_data = tfds.load(
#     name="imdb_reviews",
#     split=('train[:60%]', 'train[60%:]', 'test'),
#     as_supervised=True,
#     data_dir="D:/Datasets/tensorflow_datasets"  # 指定存储路径(可选)
# )

# # 查看训练数据的前5条
# for text, label in train_data.take(5):
#     print(f"Review: {text.numpy()}")
#     print(f"Label: {label.numpy()}")

保存数据集

# import tensorflow as tf

# # 定义保存路径
# save_dir = "D:/Datasets/imdb_reviews_saved_01"

# # 保存训练集、验证集、测试集
# tf.data.Dataset.save(train_data, f"{save_dir}/train")
# tf.data.Dataset.save(validation_data, f"{save_dir}/validation")
# tf.data.Dataset.save(test_data, f"{save_dir}/test")

加载数据集

# 重新加载数据集
import tensorflow as tf
save_dir = "D:/Datasets/imdb_reviews_saved_01"
train_data_loaded = tf.data.Dataset.load(f"{save_dir}/train")
validation_data_loaded = tf.data.Dataset.load(f"{save_dir}/validation")
test_data_loaded = tf.data.Dataset.load(f"{save_dir}/test")

# 验证数据是否正确加载
for text, label in train_data_loaded.take(1):
    print("Loaded Review:", text.numpy())
    print("Loaded Label:", label.numpy())

Loaded Review: b"This was an absolutely terrible movie. Don't be lured in by Christopher Walken or Michael Ironside. Both are great actors, but this must simply be their worst role in history. Even their great acting could not redeem this movie's ridiculous storyline. This movie is an early nineties US propaganda piece. The most pathetic scenes were those when the Columbian rebels were making their cases for revolutions. Maria Conchita Alonso appeared phony, and her pseudo-love affair with Walken was nothing but a pathetic emotional plug in a movie that was devoid of any real meaning. I am disappointed that there are movies like this, ruining actor's like Christopher Walken's good name. I could barely sit through it."
Loaded Label: 0

#导入需要的包
#pip install -i https://pypi.tuna.tsinghua.edu.cn/simple tensorflow_datasets pandas nltk scikit-learn matplotlib

逻辑回归模型

import tensorflow_datasets as tfds
import pandas as pd
import nltk
from nltk.tokenize import word_tokenize
from nltk.corpus import stopwords
from nltk.stem import WordNetLemmatizer
import re
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, classification_report
import matplotlib.pyplot as plt
import pandas as pd

# ---- 1. 数据加载 ----
def load_imdb_data():
    # 加载数据集(如果已保存,直接加载离线数据)
    try:
        train_data = tf.data.Dataset.load("D:/Datasets/imdb_reviews_saved/train")
        test_data = tf.data.Dataset.load("D:/Datasets/imdb_reviews_saved/test")
        print("从本地加载数据集成功!")
    except:
        print("从TensorFlow Datasets下载数据...")
        train_data, test_data = tfds.load(
            name="imdb_reviews",
            split=('train', 'test'),
            as_supervised=True,
            data_dir="D:/Datasets/tensorflow_datasets"
        )
        # 保存数据集以便下次直接加载
        tf.data.Dataset.save(train_data, "D:/Datasets/imdb_reviews_saved/train")
        tf.data.Dataset.save(test_data, "D:/Datasets/imdb_reviews_saved/test")
    
    return train_data, test_data

# 加载数据
train_data, test_data = load_imdb_data()

# 转换为Pandas DataFrame
def dataset_to_dataframe(dataset):
    texts, labels = [], []
    for text, label in dataset:
        texts.append(text.numpy().decode('utf-8'))
        labels.append(label.numpy())
    return pd.DataFrame({'text': texts, 'label': labels})

train_df = dataset_to_dataframe(train_data)
test_df = dataset_to_dataframe(test_data)

print("\n数据概览:")
print(f"训练集: {len(train_df)}条 | 测试集: {len(test_df)}条")
print(f"训练集正面评论比例: {train_df['label'].mean():.2f}")

# # ---- 2. 文本预处理(NLTK)----
#不用再下载了,nltk的数据已经整合成了一个包
# nltk.download('punkt')
# nltk.download('stopwords')
# nltk.download('wordnet')

def preprocess_text(text):
    # 清洗文本
    text = re.sub(r'<[^>]+>', '', text)  # 移除HTML标签
    text = re.sub(r'[^\w\s]', '', text.lower())  # 去标点+所有大写转化为小写
    
    # 分词
    tokens = word_tokenize(text)#word_tokenize 是 nltk 库中的一个函数,将输入的文本分割成一个个单词或符号,返回包含这些单词和符号的列表。
    
    # 移除停用词和词形还原
    stop_words = set(stopwords.words('english'))# 获取英语停用词集合,停用词是像 “the”“and”“is” 这类对文本语义理解贡献不大的词。
    lemmatizer = WordNetLemmatizer()#lemmatizer做词形还原,是把单词还原为其基本形式,例如把 “running” 还原成 “run”。
    tokens = [lemmatizer.lemmatize(word) for word in tokens if word not in stop_words] #移除停用词,并且对剩余单词进行词形还原
    
    return ' '.join(tokens) #' '.join(tokens):将处理后的单词列表重新组合成一个字符串,单词之间用空格分隔。

print("\n预处理示例:")
sample_text = train_df['text'].iloc[0]
print(f"原始文本: {sample_text[:100]}...")
print(f"处理后: {preprocess_text(sample_text)[:100]}...")

# 应用预处理
train_df['cleaned_text'] = train_df['text'].apply(preprocess_text)
test_df['cleaned_text'] = test_df['text'].apply(preprocess_text)

# ---- 3. 特征提取(TF-IDF)----把文本数据转换为可用于机器学习模型训练的数值特征
#max_features=5000:此参数限定了最终特征矩阵中特征的最大数量。算法会选取 TF-IDF 值最高的 5000 个特征,以此降低特征维度,避免过拟合。

#词频(TF) :衡量词在当前文档中的重要性,出现次数越多,TF 值越大
#逆文档频率(IDF):某个词在所有文档中的稀有程度,如果一个词在大多数文档中都出现(如 "the"),它的 IDF 值会很低;反之,罕见词(如 "awesome")的 IDF 值较高。
#TF-IDF 是=TF * IDF 

#ngram_range=(1, 2):表示会同时使用 1-gram 和 2-gram 特征,1-gram 指单个单词,2-gram 指相邻的两个单词组合。
#例如,对于文本 “this is a test”,1-gram 有 “this”、“is”、“a”、“test”,2-gram 有 “this is”、“is a”、“a test”。
#X_train和X_test都是稀疏矩阵
# <Compressed Sparse Row sparse matrix of dtype 'float64' with 2076135 stored elements and shape (25000, 5000)>
#这是一个 25,000 行 × 5,000 列 的矩阵,总共有 1.25 亿个元素位置,但实际存储了 2,076,135 个非零元素(即非零值),非零元素占比仅为0.0166%
#CSR 格式:仅记录非零值的位置和数值,节省内存

tfidf = TfidfVectorizer(max_features=5000, ngram_range=(1, 2))  # 使用1-gram和2-gram
#测试集必须完全基于训练集的统计信息(如 IDF 值、选定的词汇表)。如果在测试集上重新 fit,会导致模型评估结果虚高
X_train = tfidf.fit_transform(train_df['cleaned_text']) #在训练集上计算 TF-IDF 的词汇表、IDF 权重等参数,并转换训练数据
X_test = tfidf.transform(test_df['cleaned_text']) #在测试集上复用训练集学到的参数(如词汇表、IDF 值),仅进行转换。
y_train = train_df['label']
y_test = test_df['label']

###获取排名靠前的特征名称及重要性
import numpy as np
# 获取特征名称(即入选的词)
feature_names = tfidf.get_feature_names_out()
# 计算每个特征的平均重要性
average_importance = np.array(X_train.mean(axis=0)).flatten()
# 按重要性排序
sorted_indices = average_importance.argsort()[::-1]
sorted_features = feature_names[sorted_indices]
sorted_importance = average_importance[sorted_indices]

# 打印前 10 个最重要的词
for feature, importance in zip(sorted_features[:10], sorted_importance[:10]):
    print(f"Word: {feature}, Importance: {importance}")
print(f"\nTF-IDF特征维度: {X_train.shape}")

# ---- 4. 训练模型 ----
model = LogisticRegression(max_iter=1000, C=5)  # 调大C值减少正则化
model.fit(X_train, y_train)

# ---- 5. 模型评估 ----
y_pred = model.predict(X_test)
print("\n模型评估:")
print(f"准确率: {accuracy_score(y_test, y_pred):.4f}")
print("分类报告:\n", classification_report(y_test, y_pred))

# ---- 6. 可视化关键特征 ----
def plot_top_words(model, vectorizer, n=20):
    feature_names = vectorizer.get_feature_names_out()
    coef = model.coef_.ravel()
    top_positive = pd.Series(coef, index=feature_names).nlargest(n)
    top_negative = pd.Series(coef, index=feature_names).nsmallest(n)
    
    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 6))
    top_positive.plot.barh(ax=ax1, color='green')
    ax1.set_title("Top Positive Words")
    top_negative.plot.barh(ax=ax2, color='red')
    ax2.set_title("Top Negative Words")
    plt.tight_layout()
    plt.show()

plot_top_words(model, tfidf)

# ---- 7. 预测新评论 ----
def predict_sentiment(text):
    cleaned = preprocess_text(text)
    features = tfidf.transform([cleaned])
    proba = model.predict_proba(features)[0]
    sentiment = "正面" if proba[1] > 0.5 else "负面"
    print(f"文本: {text[:100]}...")
    print(f"预测: {sentiment} (置信度: {max(proba):.2f})")

# 测试示例
predict_sentiment("This movie is a masterpiece! The acting is brilliant.")
predict_sentiment("I've never seen such a boring movie in my life.")

从本地加载数据集成功!

数据概览:
训练集: 25000条 | 测试集: 25000条
训练集正面评论比例: 0.50

预处理示例:
原始文本: This was an absolutely terrible movie. Don't be lured in by Christopher Walken or Michael Ironside. ...
处理后: absolutely terrible movie dont lured christopher walken michael ironside great actor must simply wor...
Word: movie, Importance: 0.060362864044187746
Word: film, Importance: 0.05257510101759841
Word: one, Importance: 0.03132943921113504
Word: like, Importance: 0.026834183821197535
Word: good, Importance: 0.023254830854701914
Word: time, Importance: 0.022173820392865782
Word: character, Importance: 0.02151410573723059
Word: story, Importance: 0.021105128816368245
Word: really, Importance: 0.01988400654908133
Word: would, Importance: 0.01949360600852529

TF-IDF特征维度: (25000, 5000)

模型评估:
准确率: 0.8765
分类报告:
               precision    recall  f1-score   support

           0       0.88      0.88      0.88     12500
           1       0.88      0.88      0.88     12500

    accuracy                           0.88     25000
   macro avg       0.88      0.88      0.88     25000
weighted avg       0.88      0.88      0.88     25000

文本: This movie is a masterpiece! The acting is brilliant....
预测: 正面 (置信度: 0.98)
文本: I've never seen such a boring movie in my life....
预测: 负面 (置信度: 0.85)

LSTM深度学习模型(原始版)

import tensorflow_datasets as tfds
import pandas as pd
import nltk
from nltk.tokenize import word_tokenize
from nltk.corpus import stopwords
from nltk.stem import WordNetLemmatizer
import re
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, classification_report
import matplotlib.pyplot as plt
import tensorflow as tf
from tensorflow.keras.preprocessing.text import Tokenizer
from tensorflow.keras.preprocessing.sequence import pad_sequences
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Embedding, LSTM, Dense, Dropout
from tensorflow.keras.callbacks import EarlyStopping
import numpy as np

# ---- 1. 数据加载 ----
def load_imdb_data():
    # 加载数据集(如果已保存,直接加载离线数据)
    try:
        train_data = tf.data.Dataset.load("D:/Datasets/imdb_reviews_saved/train")
        test_data = tf.data.Dataset.load("D:/Datasets/imdb_reviews_saved/test")
        print("从本地加载数据集成功!")
    except:
        print("从TensorFlow Datasets下载数据...")
        train_data, test_data = tfds.load(
            name="imdb_reviews",
            split=('train', 'test'),
            as_supervised=True,
            data_dir="D:/Datasets/tensorflow_datasets"
        )
        # 保存数据集以便下次直接加载
        tf.data.Dataset.save(train_data, "D:/Datasets/imdb_reviews_saved/train")
        tf.data.Dataset.save(test_data, "D:/Datasets/imdb_reviews_saved/test")
    
    return train_data, test_data

# 加载数据
train_data, test_data = load_imdb_data()

# 转换为Pandas DataFrame
def dataset_to_dataframe(dataset):
    texts, labels = [], []
    for text, label in dataset:
        texts.append(text.numpy().decode('utf-8'))
        labels.append(label.numpy())
    return pd.DataFrame({'text': texts, 'label': labels})

train_df = dataset_to_dataframe(train_data)
test_df = dataset_to_dataframe(test_data)

print("\n数据概览:")
print(f"训练集: {len(train_df)}条 | 测试集: {len(test_df)}条")
print(f"训练集正面评论比例: {train_df['label'].mean():.2f}")

# # ---- 2. 文本预处理(NLTK)----
#不用再下载了,nltk的数据已经整合成了一个包
# nltk.download('punkt')
# nltk.download('stopwords')
# nltk.download('wordnet')

def preprocess_text(text):
    # 清洗文本
    text = re.sub(r'<[^>]+>', '', text)  # 移除HTML标签
    text = re.sub(r'[^\w\s]', '', text.lower())  # 去标点+所有大写转化为小写
    
    # 分词
    tokens = word_tokenize(text)#word_tokenize 是 nltk 库中的一个函数,将输入的文本分割成一个个单词或符号,返回包含这些单词和符号的列表。
    
    # 移除停用词和词形还原
    stop_words = set(stopwords.words('english'))# 获取英语停用词集合,停用词是像 “the”“and”“is” 这类对文本语义理解贡献不大的词。
    lemmatizer = WordNetLemmatizer()#lemmatizer做词形还原,是把单词还原为其基本形式,例如把 “running” 还原成 “run”。
    tokens = [lemmatizer.lemmatize(word) for word in tokens if word not in stop_words] #移除停用词,并且对剩余单词进行词形还原
    
    return ' '.join(tokens) #' '.join(tokens):将处理后的单词列表重新组合成一个字符串,单词之间用空格分隔。

print("\n预处理示例:")
sample_text = train_df['text'].iloc[0]
print(f"原始文本: {sample_text[:100]}...")
print(f"处理后: {preprocess_text(sample_text)[:100]}...")

# 应用预处理
train_df['cleaned_text'] = train_df['text'].apply(preprocess_text)
test_df['cleaned_text'] = test_df['text'].apply(preprocess_text)

# ---- 3. 特征提取(TF-IDF)----把文本数据转换为可用于机器学习模型训练的数值特征
#max_features=5000:此参数限定了最终特征矩阵中特征的最大数量。算法会选取 TF-IDF 值最高的 5000 个特征,以此降低特征维度,避免过拟合。

#词频(TF) :衡量词在当前文档中的重要性,出现次数越多,TF 值越大
#逆文档频率(IDF):某个词在所有文档中的稀有程度,如果一个词在大多数文档中都出现(如 "the"),它的 IDF 值会很低;反之,罕见词(如 "awesome")的 IDF 值较高。
#TF-IDF 是=TF * IDF 

#ngram_range=(1, 2):表示会同时使用 1-gram 和 2-gram 特征,1-gram 指单个单词,2-gram 指相邻的两个单词组合。
#例如,对于文本 “this is a test”,1-gram 有 “this”、“is”、“a”、“test”,2-gram 有 “this is”、“is a”、“a test”。
#X_train和X_test都是稀疏矩阵
# <Compressed Sparse Row sparse matrix of dtype 'float64' with 2076135 stored elements and shape (25000, 5000)>
#这是一个 25,000 行 × 5,000 列 的矩阵,总共有 1.25 亿个元素位置,但实际存储了 2,076,135 个非零元素(即非零值),非零元素占比仅为0.0166%
#CSR 格式:仅记录非零值的位置和数值,节省内存




# 获取特征名称(即入选的词)
feature_names = tfidf.get_feature_names_out()
# 计算每个特征的平均重要性
average_importance = np.array(X_train.mean(axis=0)).flatten()
# 按重要性排序
sorted_indices = average_importance.argsort()[::-1]
sorted_features = feature_names[sorted_indices]
sorted_importance = average_importance[sorted_indices]

# 打印前 10 个最重要的词
for feature, importance in zip(sorted_features[:10], sorted_importance[:10]):
    print(f"Word: {feature}, Importance: {importance}")
print(f"\nTF-IDF特征维度: {X_train.shape}")


# ---- 2. 文本序列化和填充 ----
# 设置参数
VOCAB_SIZE = 5000      # 词汇表大小(与TF-IDF的max_features一致),使用出现频率最高的 5000 个单词
MAX_LEN = 100          # 每条评论允许的最大单词数(超过截断,不足填充)
EMBEDDING_DIM = 128    # 词向量维度,每个单词在嵌入层将被转换为的向量的长度

# 初始化Tokenizer并拟合训练数据
#num_words 指定词汇表大小,oov_token 代表未在词汇表中的单词。fit_on_texts 方法用于统计训练数据里单词的出现频率。
tokenizer = Tokenizer(num_words=VOCAB_SIZE, oov_token="<OOV>")
tokenizer.fit_on_texts(train_df['cleaned_text'])

# 将文本转换为序列
#texts_to_sequences 方法把文本转换为整数序列,每个整数对应词汇表中的一个单词
train_sequences = tokenizer.texts_to_sequences(train_df['cleaned_text'])
test_sequences = tokenizer.texts_to_sequences(test_df['cleaned_text'])

# 填充序列到统一长度
#pad_sequences 方法把序列填充到统一长度,padding='post' 表示在序列末尾填充,truncating='post' 表示在序列末尾截断
X_train = pad_sequences(train_sequences, maxlen=MAX_LEN, padding='post', truncating='post')
X_test = pad_sequences(test_sequences, maxlen=MAX_LEN, padding='post', truncating='post')
y_train = np.array(train_df['label'])
y_test = np.array(test_df['label'])

# ---- 3. 构建LSTM模型 ----
#Embedding 层:将输入的整数序列转换为固定长度的词向量,比如被映射为类似 [0.2, -0.5, 0.7, ...] 的向量
# input_dim:词汇表大小(VOCAB_SIZE=5000),即最多处理5000个不同的单词。
# output_dim:词向量的维度(EMBEDDING_DIM=128),每个单词用128维向量表示。
# input_length:输入序列的长度(MAX_LEN=100),每条评论被填充/截断为100个单词。
#原因:直接使用单词索引无法表达语义关系,而词向量能捕捉单词之间的相似性(如 "good" 和 "great" 的向量接近)。

#LSTM 层:包含 64 个 LSTM 单元,dropout 和 recurrent_dropout 用于防止过拟合。
# units=64:LSTM隐藏状态的维度(64维),可理解为模型的记忆容量。
# dropout=0.2:在输入层随机丢弃20%,防止过拟合。
# recurrent_dropout=0.2:在循环层(时间步之间)随机丢弃20%的连接。
# 输入:形状为 (batch_size, MAX_LEN, EMBEDDING_DIM) 的张量(即Embedding层的输出)。
# 输出:默认返回最后一个时间步的隐藏状态,形状为 (batch_size, 64)。

#Dense 层:使用sigmoid函数,用于二分类任务。
# 将LSTM的输出映射为分类结果(正面/负面)。
# 例如:将64维的LSTM输出转换为一个0~1之间的概率值。

# 输入文本 → 被转换为单词索引序列(如 [5, 128, 74, ...])。
# Embedding层 (词嵌入层)→ 将索引映射为词向量(如 [[0.1, 0.3, ...], [-0.2, 0.4, ...], ...])。(翻译成机器理解的向量语言)
# LSTM层(长短期记忆层) → 处理词向量序列,输出最终隐藏状态(捕捉整个句子的语义)。(理解并记住上下文)
# Dense层 → 将隐藏状态转换为概率值(如 0.87 表示87%概率为正面)。(根据理解的内容判断情感倾向)

# "Great movie!" 
# → 转换为索引 [12, 345](假设"great"=12,"movie"=345)
# → Embedding层变为 [[0.1, 0.3,...], [-0.2, 0.4,...]](两个128维向量)
# → LSTM层分析后输出64维的"理解向量"
# → Dense层输出0.87(87%概率是正面)

#模型架构构建
model = Sequential([
    Embedding(input_dim=5000, output_dim=128, input_length=100),
    LSTM(units=64, dropout=0.2, recurrent_dropout=0.2),  # 64个LSTM单元,添加Dropout防过拟合,防止模型像背答案一样记住训练数据
    Dense(1, activation='sigmoid')  # 二分类输出
])#数据转换为模型能理解的内容

#模型配置
model.compile(
    optimizer='adam',#优化算法,用于调整模型权重
    loss='binary_crossentropy',#损失函数,用于计算预测值与真实值的误差
    metrics=['accuracy']
)#匹配任务类型及告诉模型如何学习

#提前调用 summary() 或自定义训练时,必须显式调用
model.build(input_shape=(None, MAX_LEN))  # 显式构建模型
# 打印模型结构
model.summary()

# ---- 4. 训练模型 ----
# monitor='val_loss':监控验证集的损失值(validation loss)。
# patience=3:如果验证集损失连续3个epoch没有下降,则停止训练。
# restore_best_weights=True:恢复训练过程中验证集损失最小的模型权重(而不是最后一个epoch的权重)。

# monitor='val_loss':监控验证集的损失值(validation loss)
# patience=3:如果验证集损失连续3个epoch没有下降,则停止训练
# restore_best_weights=True:恢复训练过程中验证集损失最小的模型权重(而不是最后一个epoch的权重)

early_stopping = EarlyStopping(monitor='val_loss', patience=3, restore_best_weights=True)

history = model.fit(
    X_train, y_train,
    epochs=10,
    batch_size=64,
    validation_data=(X_test, y_test),
    callbacks=[early_stopping],
    verbose=1
)

# ---- 5. 评估模型 ----
# 绘制训练曲线
plt.figure(figsize=(12, 4))
plt.subplot(1, 2, 1)
plt.plot(history.history['accuracy'], label='Train Accuracy')
plt.plot(history.history['val_accuracy'], label='Validation Accuracy')
plt.legend()
plt.title('Accuracy over Epochs')

plt.subplot(1, 2, 2)
plt.plot(history.history['loss'], label='Train Loss')
plt.plot(history.history['val_loss'], label='Validation Loss')
plt.legend()
plt.title('Loss over Epochs')
plt.show()

# 测试集评估
test_loss, test_acc = model.evaluate(X_test, y_test, verbose=0)
print(f"\n测试集准确率: {test_acc:.4f}")

# ---- 6. 预测新评论 ----
def predict_sentiment_lstm(text):
    cleaned = preprocess_text(text)
    sequence = tokenizer.texts_to_sequences([cleaned])
    padded = pad_sequences(sequence, maxlen=MAX_LEN, padding='post', truncating='post')
    proba = model.predict(padded, verbose=0)[0][0]
    sentiment = "正面" if proba > 0.5 else "负面"
    confidence = proba if proba > 0.5 else 1 - proba
    print(f"文本: {text[:100]}...")
    print(f"预测: {sentiment} (置信度: {confidence:.2f})")

# 测试示例
predict_sentiment_lstm("This movie is a masterpiece! The acting is brilliant.")
predict_sentiment_lstm(" Terrible plot and awful acting. Waste of time.")

从本地加载数据集成功!

数据概览:
训练集: 25000条 | 测试集: 25000条
训练集正面评论比例: 0.50

预处理示例:
原始文本: This was an absolutely terrible movie. Don't be lured in by Christopher Walken or Michael Ironside. ...
处理后: absolutely terrible movie dont lured christopher walken michael ironside great actor must simply wor...
Word: movie, Importance: 0.060362864044187746
Word: film, Importance: 0.05257510101759841
Word: one, Importance: 0.03132943921113504
Word: like, Importance: 0.026834183821197535
Word: good, Importance: 0.023254830854701914
Word: time, Importance: 0.022173820392865782
Word: character, Importance: 0.02151410573723059
Word: story, Importance: 0.021105128816368245
Word: really, Importance: 0.01988400654908133
Word: would, Importance: 0.01949360600852529

TF-IDF特征维度: (25000, 5000)


D:\Anaconda\envs\nlp_env\lib\site-packages\keras\src\layers\core\embedding.py:90: UserWarning: Argument `input_length` is deprecated. Just remove it.
  warnings.warn(

┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┓
┃ Layer (type)                         ┃ Output Shape                ┃         Param # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━┩
│ embedding (Embedding)                │ (None, 100, 128)            │         640,000 │
├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤
│ lstm (LSTM)                          │ (None, 64)                  │          49,408 │
├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤
│ dense (Dense)                        │ (None, 1)                   │              65 │
└──────────────────────────────────────┴─────────────────────────────┴─────────────────┘

Epoch 1/10
391/391 ━━━━━━━━━━━━━━━━━━━━ 29s 68ms/step - accuracy: 0.5567 - loss: 0.6784 - val_accuracy: 0.6371 - val_loss: 0.6553
Epoch 2/10
391/391 ━━━━━━━━━━━━━━━━━━━━ 26s 66ms/step - accuracy: 0.6751 - loss: 0.6190 - val_accuracy: 0.6833 - val_loss: 0.5997
Epoch 3/10
391/391 ━━━━━━━━━━━━━━━━━━━━ 30s 77ms/step - accuracy: 0.7404 - loss: 0.5471 - val_accuracy: 0.7911 - val_loss: 0.4949
Epoch 4/10
391/391 ━━━━━━━━━━━━━━━━━━━━ 31s 79ms/step - accuracy: 0.8196 - loss: 0.4430 - val_accuracy: 0.8196 - val_loss: 0.4350
Epoch 5/10
391/391 ━━━━━━━━━━━━━━━━━━━━ 27s 69ms/step - accuracy: 0.8576 - loss: 0.3568 - val_accuracy: 0.8306 - val_loss: 0.3999
Epoch 6/10
391/391 ━━━━━━━━━━━━━━━━━━━━ 29s 75ms/step - accuracy: 0.8869 - loss: 0.2944 - val_accuracy: 0.8379 - val_loss: 0.3843
Epoch 7/10
391/391 ━━━━━━━━━━━━━━━━━━━━ 29s 73ms/step - accuracy: 0.8993 - loss: 0.2653 - val_accuracy: 0.8402 - val_loss: 0.3906
Epoch 8/10
391/391 ━━━━━━━━━━━━━━━━━━━━ 27s 69ms/step - accuracy: 0.9171 - loss: 0.2262 - val_accuracy: 0.8096 - val_loss: 0.4335
Epoch 9/10
391/391 ━━━━━━━━━━━━━━━━━━━━ 29s 74ms/step - accuracy: 0.8679 - loss: 0.3054 - val_accuracy: 0.8381 - val_loss: 0.4069

测试集准确率: 0.8379
文本: This movie is a masterpiece! The acting is brilliant....
预测: 正面 (置信度: 0.94)
文本: Terrible plot and awful acting. Waste of time....
预测: 负面 (置信度: 0.97)

┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┓ ┃ Layer (type) ┃ Output Shape ┃ Param # ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━┩ │ embedding_2 (Embedding) │ (None, 100, 128) │ 640,000 │ ├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤ │ lstm_2 (LSTM) │ (None, 64) │ 49,408 │ ├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤ │ dense_2 (Dense) │ (None, 1) │ 65 │ └──────────────────────────────────────┴─────────────────────────────┴─────────────────┘ Total params: 689,473 (2.63 MB) Trainable params: 689,473 (2.63 MB) Non-trainable params: 0 (0.00 B)

Layer (type) 层名称和类型(如Embedding、LSTM)。 Output Shape 该层的输出张量形状。None表示批量大小可变(如(None, 100, 128)表示(batch_size, 序列长度, 词向量维度))。 Param # 该层的可训练参数数量。例如:

  • Embedding层:5000词汇 × 128维 = 640,000参数
  • LSTM层:4 × (128输入 + 64隐藏) × 64单元 = 49,408参数 Trainable params 总需训练的参数量,帮助判断模型是否过大。

【图形解读】: 左侧 “Accuracy over Epochs” 图 横坐标(x 轴):表示训练轮次(Epoch),从 0 到 7,反映训练进行的阶段 。 纵坐标(y 轴):表示准确率,范围是 0.5 - 0.9,体现模型预测正确的比例 。 曲线: 蓝色曲线(Train Accuracy):代表训练集上的准确率。 橙色曲线(Validation Accuracy):代表验证集上的准确率。 右侧 “Loss over Epochs” 图 横坐标(x 轴):同样是训练轮次(Epoch),范围 0 - 7 。 纵坐标(y 轴):表示损失值,范围 0.2 - 0.7 ,损失值越低代表模型预测结果与真实结果越接近。 曲线: 蓝色曲线(Train Loss):是训练集的损失值曲线 橙色曲线(Validation Loss):为验证集的损失值曲线。

LSTM深度学习模型(函数式API版)

import tensorflow_datasets as tfds
import pandas as pd
import nltk
from nltk.tokenize import word_tokenize
from nltk.corpus import stopwords
from nltk.stem import WordNetLemmatizer
import re
import numpy as np
import matplotlib.pyplot as plt
import tensorflow as tf
from tensorflow.keras import Input, Model
from tensorflow.keras.layers import Embedding, LSTM, Dense
from tensorflow.keras.preprocessing.text import Tokenizer
from tensorflow.keras.preprocessing.sequence import pad_sequences
from tensorflow.keras.callbacks import EarlyStopping

# ---- 1. 数据加载 ----
def load_imdb_data():
    try:
        train_data = tf.data.Dataset.load("D:/Datasets/imdb_reviews_saved/train")
        test_data = tf.data.Dataset.load("D:/Datasets/imdb_reviews_saved/test")
        print("从本地加载数据集成功!")
    except:
        print("从TensorFlow Datasets下载数据...")
        train_data, test_data = tfds.load(
            name="imdb_reviews",
            split=('train', 'test'),
            as_supervised=True,
            data_dir="D:/Datasets/tensorflow_datasets"
        )
        tf.data.Dataset.save(train_data, "D:/Datasets/imdb_reviews_saved/train")
        tf.data.Dataset.save(test_data, "D:/Datasets/imdb_reviews_saved/test")
    return train_data, test_data

# 加载数据
train_data, test_data = load_imdb_data()

# 转换为Pandas DataFrame
def dataset_to_dataframe(dataset):
    texts, labels = [], []
    for text, label in dataset:
        texts.append(text.numpy().decode('utf-8'))
        labels.append(label.numpy())
    return pd.DataFrame({'text': texts, 'label': labels})

train_df = dataset_to_dataframe(train_data)
test_df = dataset_to_dataframe(test_data)

print("\n数据概览:")
print(f"训练集: {len(train_df)}条 | 测试集: {len(test_df)}条")
print(f"训练集正面评论比例: {train_df['label'].mean():.2f}")

# ---- 2. 文本预处理 ----
def preprocess_text(text):
    text = re.sub(r'<[^>]+>', '', text)  # 移除HTML标签
    text = re.sub(r'[^\w\s]', '', text.lower())  # 去标点+转小写
    tokens = word_tokenize(text)
    stop_words = set(stopwords.words('english'))
    lemmatizer = WordNetLemmatizer()
    tokens = [lemmatizer.lemmatize(word) for word in tokens if word not in stop_words]
    return ' '.join(tokens)

print("\n预处理示例:")
sample_text = train_df['text'].iloc[0]
print(f"原始文本: {sample_text[:100]}...")
print(f"处理后: {preprocess_text(sample_text)[:100]}...")

train_df['cleaned_text'] = train_df['text'].apply(preprocess_text)
test_df['cleaned_text'] = test_df['text'].apply(preprocess_text)

# ---- 3. 文本序列化 ----
VOCAB_SIZE = 5000
MAX_LEN = 100
EMBEDDING_DIM = 128

tokenizer = Tokenizer(num_words=VOCAB_SIZE, oov_token="<OOV>")
tokenizer.fit_on_texts(train_df['cleaned_text'])

train_sequences = tokenizer.texts_to_sequences(train_df['cleaned_text'])
test_sequences = tokenizer.texts_to_sequences(test_df['cleaned_text'])

X_train = pad_sequences(train_sequences, maxlen=MAX_LEN, padding='post', truncating='post')
X_test = pad_sequences(test_sequences, maxlen=MAX_LEN, padding='post', truncating='post')
y_train = np.array(train_df['label'])
y_test = np.array(test_df['label'])

# ---- 4. 使用函数式API构建模型 ----
# 定义输入层
inputs = Input(shape=(MAX_LEN,))

# 构建网络
embedding = Embedding(input_dim=VOCAB_SIZE, 
                     output_dim=EMBEDDING_DIM, 
                     input_length=MAX_LEN)(inputs)
lstm = LSTM(64, dropout=0.2, recurrent_dropout=0.2)(embedding)
outputs = Dense(1, activation='sigmoid')(lstm)

# 创建模型
model = Model(inputs=inputs, outputs=outputs)

# 编译模型
model.compile(optimizer='adam',
              loss='binary_crossentropy',
              metrics=['accuracy'])

model.summary()

# ---- 5. 训练模型 ----
early_stopping = EarlyStopping(monitor='val_loss', patience=3, restore_best_weights=True)

history = model.fit(
    X_train, y_train,
    epochs=10,
    batch_size=64,
    validation_data=(X_test, y_test),
    callbacks=[early_stopping],
    verbose=1
)

# ---- 6. 评估模型 ----
def plot_history(history):
    plt.figure(figsize=(12, 4))
    plt.subplot(1, 2, 1)
    plt.plot(history.history['accuracy'], label='Train Accuracy')
    plt.plot(history.history['val_accuracy'], label='Validation Accuracy')
    plt.legend()
    plt.title('Accuracy over Epochs')

    plt.subplot(1, 2, 2)
    plt.plot(history.history['loss'], label='Train Loss')
    plt.plot(history.history['val_loss'], label='Validation Loss')
    plt.legend()
    plt.title('Loss over Epochs')
    plt.show()

plot_history(history)

test_loss, test_acc = model.evaluate(X_test, y_test, verbose=0)
print(f"\n测试集准确率: {test_acc:.4f}")

# ---- 7. 预测新评论 ----
# model 是之前训练好的深度学习模型。model.predict 方法对填充后的序列 padded 进行预测,
# 返回一个包含预测概率的数组。verbose=0 表示在预测过程中不输出详细信息。由于 padded 只包含一个样本,所以取 [0][0] 来获取该样本的预测概率 proba。

def predict_sentiment(text):
    cleaned = preprocess_text(text)
    sequence = tokenizer.texts_to_sequences([cleaned])
    padded = pad_sequences(sequence, maxlen=MAX_LEN, padding='post', truncating='post')
    proba = model.predict(padded, verbose=0)[0][0]
    sentiment = "正面" if proba > 0.5 else "负面"
    confidence = proba if proba > 0.5 else 1 - proba
    print(f"文本: {text[:100]}{'...' if len(text)>100 else ''}")
    print(f"预测: {sentiment} (置信度: {confidence:.2f})")

# 测试示例
test_samples = [
    "This movie is a masterpiece! The acting is brilliant.",
    "Terrible plot and awful acting. Waste of time.",
    "The film was okay, not great but not bad either.",
    "I've never seen such a boring movie in my life."
]

for sample in test_samples:
    predict_sentiment(sample)
    print("-" * 50)

从本地加载数据集成功!

数据概览:
训练集: 25000条 | 测试集: 25000条
训练集正面评论比例: 0.50

预处理示例:
原始文本: This was an absolutely terrible movie. Don't be lured in by Christopher Walken or Michael Ironside. ...
处理后: absolutely terrible movie dont lured christopher walken michael ironside great actor must simply wor...


D:\Anaconda\envs\nlp_env\lib\site-packages\keras\src\layers\core\embedding.py:90: UserWarning: Argument `input_length` is deprecated. Just remove it.
  warnings.warn(

┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┓
┃ Layer (type)                         ┃ Output Shape                ┃         Param # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━┩
│ input_layer_1 (InputLayer)           │ (None, 100)                 │               0 │
├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤
│ embedding_1 (Embedding)              │ (None, 100, 128)            │         640,000 │
├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤
│ lstm_1 (LSTM)                        │ (None, 64)                  │          49,408 │
├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤
│ dense_1 (Dense)                      │ (None, 1)                   │              65 │
└──────────────────────────────────────┴─────────────────────────────┴─────────────────┘

Epoch 1/10
391/391 ━━━━━━━━━━━━━━━━━━━━ 34s 82ms/step - accuracy: 0.5601 - loss: 0.6772 - val_accuracy: 0.6172 - val_loss: 0.6550
Epoch 2/10
391/391 ━━━━━━━━━━━━━━━━━━━━ 31s 79ms/step - accuracy: 0.6355 - loss: 0.6389 - val_accuracy: 0.7319 - val_loss: 0.5710
Epoch 3/10
391/391 ━━━━━━━━━━━━━━━━━━━━ 30s 77ms/step - accuracy: 0.7464 - loss: 0.5361 - val_accuracy: 0.8206 - val_loss: 0.4069
Epoch 4/10
391/391 ━━━━━━━━━━━━━━━━━━━━ 30s 77ms/step - accuracy: 0.8584 - loss: 0.3455 - val_accuracy: 0.8310 - val_loss: 0.3904
Epoch 5/10
391/391 ━━━━━━━━━━━━━━━━━━━━ 30s 78ms/step - accuracy: 0.8996 - loss: 0.2713 - val_accuracy: 0.8385 - val_loss: 0.3850
Epoch 6/10
391/391 ━━━━━━━━━━━━━━━━━━━━ 32s 81ms/step - accuracy: 0.9149 - loss: 0.2361 - val_accuracy: 0.8364 - val_loss: 0.4060
Epoch 7/10
391/391 ━━━━━━━━━━━━━━━━━━━━ 30s 77ms/step - accuracy: 0.9339 - loss: 0.1938 - val_accuracy: 0.8351 - val_loss: 0.4239
Epoch 8/10
391/391 ━━━━━━━━━━━━━━━━━━━━ 29s 75ms/step - accuracy: 0.9424 - loss: 0.1701 - val_accuracy: 0.8291 - val_loss: 0.4592

测试集准确率: 0.8385
文本: This movie is a masterpiece! The acting is brilliant.
预测: 正面 (置信度: 0.93)
--------------------------------------------------
文本: Terrible plot and awful acting. Waste of time.
预测: 负面 (置信度: 0.96)
--------------------------------------------------
文本: The film was okay, not great but not bad either.
预测: 负面 (置信度: 0.77)
--------------------------------------------------
文本: I've never seen such a boring movie in my life.
预测: 负面 (置信度: 0.52)
--------------------------------------------------

附:原始文本数据如何序列化

#序列化逻辑: 原句子 →正则表达式去掉标点符号 →nltk的word_tokenize函数把连续文本拆成独立词汇 →去掉nltk里的停用词(比如“the”“and”)等频繁出现但意义不大的词 →tensorflow的tokenizer方法对所有词按频率排序,保留出现频率最高的前 num_words 个词,其余的词统一替换为(未登录词) →tokenizer.word_index看到每个词对应的索引,如{'': 1, 'bad': 2, 'movie': 3, 'great': 4} →文本转序列:texts_to_sequences 是 Tokenizer 类的一个方法,用其把每个文本中的单词替换成其在词汇表中对应的索引。'movie great loved' → [3, 4, 1] →tensorflow里pad_sequences 方法将上面的序列依次填充到统一的长度,比如3 4 1 0 0 →

import pandas as pd
import re
from nltk.tokenize import word_tokenize
from nltk.corpus import stopwords
from nltk.stem import WordNetLemmatizer
from tensorflow.keras.preprocessing.text import Tokenizer
from tensorflow.keras.preprocessing.sequence import pad_sequences
import numpy as np

# ---- 1. 构造微型数据集 ----
data = {
    "text": [
        "This <b>movie</b> was GREAT! I loved it.",  # 正面
        "Terrible plot, waste of time.",            # 负面
        "Not bad, bad, but the acting is mediocre."      # 混合
    ],
    "label": [1, 0, 0]
}
mini_df = pd.DataFrame(data)

# ---- 2. 预处理函数 ----
stop_words = set(stopwords.words('english'))
lemmatizer = WordNetLemmatizer()

def preprocess_text(text):
    text = re.sub(r'<[^>]+>', '', text)          # 去HTML
    text = re.sub(r'[^\w\s]', '', text.lower())  # 去标点+小写
    tokens = word_tokenize(text)                 # 分词,把连续的文本拆分成一个个独立词汇
    tokens = [lemmatizer.lemmatize(word) for word in tokens if word not in stop_words] #stop_words停用词: “the”“and”“is”“in”,一般会删除掉这些停用词
    return ' '.join(tokens)

# ---- 3. 逐步转换演示 ----
print("="*50 + "\n原始数据:\n" + "="*50)
print(mini_df)

# 步骤1: 文本预处理
mini_df['cleaned_text'] = mini_df['text'].apply(preprocess_text)
print("\n" + "="*50 + "\n预处理后 (cleaned_text):\n" + "="*50)
print(mini_df[['text', 'cleaned_text']])

# 步骤2: 构建词汇表 (限制5个词)
#tensorflow.keras:深度学习框架,Tokenizer 用于构建词汇表和将文本转换为序列
#Tokenizer:创建一个 Tokenizer 对象,先计算每个词出现的频率,然后按照索引排序,后续在tokenizer.word_index可以取到
#num_words=5 表示只保留前 5 个高频词,oov_token="<OOV>" 表示将不在词汇表中的词替换为 <OOV>
# Tokenizer 会根据文本中词的出现频率对所有词进行排序,然后只保留出现频率最高的前 num_words 个词。num_words=5 意味着只保留出现频率最高的 5 个词,
#其余的词会被当作未登录词(Out - Of - Vocabulary,OOV)处理。这样可以减少词汇表的大小,从而降低模型的复杂度和计算量,同时也有助于避免过拟合
tokenizer = Tokenizer(num_words=5, oov_token="<OOV>")


# 在 tensorflow.keras 的 Tokenizer 类中,fit_on_texts 方法会根据输入文本中单词的出现频率来为每个单词分配一个索引,
# 并且 word_index 属性存储的索引是按照单词出现频率从高到低进行排序的
tokenizer.fit_on_texts(mini_df['cleaned_text'])
word_index = tokenizer.word_index
print("\n" + "="*50 + "\n完整词汇表统计 (所有词):\n" + "="*50)
print(word_index)  # 显示所有词的统计(仅用于观察)
print("\n" + "="*50 + "\n实际生效的词汇表 (前5个高频词 + <OOV>):\n" + "="*50)
effective_vocab = {k:v for k,v in word_index.items() if v <= 5}  # 只显示索引<=5的词
print(effective_vocab)
print("注意:其他词在序列化时会被替换为 <OOV> (索引1)")

# 步骤3: 文本转序列
# texts_to_sequences 是 Tokenizer 类的一个方法,把每个文本中的单词替换成其在词汇表中对应的索引。
# 如果某个单词在词汇表中,就用其对应的索引替代;如果不在词汇表中(即未登录词,OOV),就用之前设定的 oov_token 对应的索引(这里是 <OOV>,索引为 1)来替代。
#这里的sequences结果是[[3, 4, 1], [1, 1, 1, 1], [2, 2, 1, 1]]

# 'movie great loved' → [3, 4, 1] (解释: ['movie→3', 'great→4', 'loved→1'])
# 'terrible plot waste time' → [1, 1, 1, 1] (解释: ['terrible→1', 'plot→1', 'waste→1', 'time→1'])
# 'bad bad acting mediocre' → [2, 2, 1, 1] (解释: ['bad→2', 'bad→2', 'acting→1', 'mediocre→1'])

sequences = tokenizer.texts_to_sequences(mini_df['cleaned_text'])
print("\n" + "="*50 + "\n序列化结果 (sequences):\n" + "="*50)
# 将每条文本及其对应的整数序列打印出来,并给出每个单词和其对应索引的解释
for text, seq in zip(mini_df['cleaned_text'], sequences):
    print(f"'{text}' → {seq} (解释: {[f'{w}→{i}' for w,i in zip(text.split(), seq)]})")

# 步骤4: 序列填充 (统一长度为5)
#pad_sequences 函数会遍历输入的 sequences 列表中的每个序列,将sequences 填充或截断成统一的长度
#padding='post':指定填充的位置。'post' 表示在序列的末尾进行填充
#truncating='post':指定截断的位置。'post' 表示如果序列的长度超过 maxlen,则从序列的末尾开始截断
X_demo = pad_sequences(sequences, maxlen=5, padding='post', truncating='post')
print("\n" + "="*50 + "\n填充后序列 (X_demo):\n" + "="*50)
print(X_demo)
print("形状:", X_demo.shape)

# 步骤5: 标签处理
y_demo = np.array(mini_df['label'])
print("\n" + "="*50 + "\n标签 (y_demo):\n" + "="*50)
print(y_demo)

# ---- 4. 验证OOV处理 ----
test_text = "movie and great and unknownword"
test_seq = tokenizer.texts_to_sequences([test_text])[0]
print("\n" + "="*50 + "\nOOV测试:\n" + "="*50)
print(f"测试文本: '{test_text}'")
print(f"序列化结果: {test_seq} (解释: 'and'和'unknownword'→1)")

==================================================
原始数据:
==================================================
                                        text  label
0   This <b>movie</b> was GREAT! I loved it.      1
1              Terrible plot, waste of time.      0
2  Not bad, bad, but the acting is mediocre.      0

==================================================
预处理后 (cleaned_text):
==================================================
                                        text              cleaned_text
0   This <b>movie</b> was GREAT! I loved it.         movie great loved
1              Terrible plot, waste of time.  terrible plot waste time
2  Not bad, bad, but the acting is mediocre.   bad bad acting mediocre

==================================================
完整词汇表统计 (所有词):
==================================================
{'<OOV>': 1, 'bad': 2, 'movie': 3, 'great': 4, 'loved': 5, 'terrible': 6, 'plot': 7, 'waste': 8, 'time': 9, 'acting': 10, 'mediocre': 11}

==================================================
实际生效的词汇表 (前5个高频词 + <OOV>):
==================================================
{'<OOV>': 1, 'bad': 2, 'movie': 3, 'great': 4, 'loved': 5}
注意:其他词在序列化时会被替换为 <OOV> (索引1)

==================================================
序列化结果 (sequences):
==================================================
'movie great loved' → [3, 4, 1] (解释: ['movie→3', 'great→4', 'loved→1'])
'terrible plot waste time' → [1, 1, 1, 1] (解释: ['terrible→1', 'plot→1', 'waste→1', 'time→1'])
'bad bad acting mediocre' → [2, 2, 1, 1] (解释: ['bad→2', 'bad→2', 'acting→1', 'mediocre→1'])

==================================================
填充后序列 (X_demo):
==================================================
[[3 4 1 0 0]
 [1 1 1 1 0]
 [2 2 1 1 0]]
形状: (3, 5)

==================================================
标签 (y_demo):
==================================================
[1 0 0]

==================================================
OOV测试:
==================================================
测试文本: 'movie and great and unknownword'
序列化结果: [3, 1, 4, 1, 1] (解释: 'and'和'unknownword'→1)

附:LSTM如何处理序列化数据

import numpy as np
from tensorflow.keras.models import Model
from tensorflow.keras.layers import Input, Embedding, LSTM, Dense

# ---- 1. 准备输入数据 ----
print("="*50 + "\n输入数据:\n" + "="*50)

# 取第一个样本 [3, 4, 1, 0, 0]
input_data = np.array([[3, 4, 1, 0, 0]])
print(input_data)
print("形状:", input_data.shape)

# ---- 2. 重新构建模型(使用函数式API更可靠) ----
print("\n" + "="*50 + "\n构建模型:\n" + "="*50)

#
#嵌入层的主要功能是把输入的离散词汇(通常用整数表示)转换为连续的向量表示。这种向量表示能够捕捉词汇之间的语义关系,比如相似的词汇在向量空间中距离较近
#EMBEDDING_DIM 确定了每个词汇经过嵌入层转换后得到的向量的维度。EMBEDDING_DIM = 4 意味着每个词汇会被转换为一个 4 维的向量。
#例如,若输入的是 [3, 4, 1, 0, 0],那么每个整数(代表一个词汇)都会被转换为一个 4 维向量中的点,像 3 可能会被转换为 [0.2, -0.1, 0.5, 0.3]。

EMBEDDING_DIM = 4 #嵌入层(Embedding layer)的输出维度
#LSTM 层通过内部的门控机制来控制信息,从而更好地捕捉序列中的长期依赖关系。
#LSTM_UNITS 指的是 LSTM 层中单元的数量。每个 LSTM 单元可以看作是一个小型的神经网络,它接收输入并产生输出
#LSTM_UNITS = 3 表示 LSTM 层中有 3 个单元。这意味着 LSTM 层在处理输入序列时,会输出一个长度为 3 的向量作为隐藏状态,如输入序列经过处理后, → [0.1, -0.2, 0.3]

LSTM_UNITS = 3 #长短期记忆网络层(LSTM layer)的单元数量
# 定义输入序列的最大长度
MAX_LEN = 5  # 输入数据的长度

#在 Keras 的函数式 API 里,构建模型时要先定义输入层,再将其作为后续各层的输入,以此逐步搭建整个模型。这行代码的作用就是定义模型的输入层。
#Input 类:它是 Keras 里用来定义输入层的类。借助这个类,能够【明确模型输入数据的形状和其他相关属性】。
# shape=(MAX_LEN,):shape 参数用来指定输入数据的形状。MAX_LEN 代表输入序列的最大长度,在处理文本数据时,需要把所有输入序列填充或者截断成相同的长度MAX_LEN。
#(MAX_LEN,) 表示输入数据是一个一维的序列,其长度为 MAX_LEN。例如,若 MAX_LEN 是 100,那么输入的数据就是长度为 100 的一维向量。
# name="input":name 参数为输入层赋予一个名称,方便后续在模型可视化或者调试时识别该层。在查看模型结构或者打印模型摘要时,就能通过这个名称快速定位到输入层。
input_layer = Input(shape=(MAX_LEN,), name="input")

#input_dim=5 表示词汇表的大小,即总共有 5 个词汇(包括 <OOV> 和填充值等)
#input_length=MAX_LEN:输入到嵌入层的整数序列的长度。如MAX_LEN 为 5,序列长度就为5,,比如 [3, 4, 1, 0, 0]
#和前面的单词转化为序列用到的函数一致,pad_sequences(train_sequences, maxlen=MAX_LEN, padding='post', truncating='post')
#(input_layer):表示将前面定义的输入层 input_layer 作为嵌入层的输入,即嵌入层会对输入层的整数序列进行处理,将每个整数转换为对应的 4 维向量。
# 这里是在定义进入嵌入层的数据的格式
#神经网络无法直接处理文本数据,需要将文本中的每个词转换为向量形式。Embedding 层的作用就是将输入的整数(每个整数代表一个词)映射为固定长度的密集向量。
embedding = Embedding(input_dim=5, output_dim=EMBEDDING_DIM, 
                     input_length=MAX_LEN, name="embedding")(input_layer)

#units=LSTM_UNITS:LSTM 层会输出一个长度为 3 的向量作为隐藏状态,如[0.1, -0.2, 0.3]
# dropout=0.2:在训练过程中,以 0.2 的概率随机忽略输入单元,防止过拟合。
# recurrent_dropout=0.2:在训练过程中,以 0.2 的概率随机忽略循环连接的单元,同样是为了防止过拟合。
# name="lstm":为该层指定一个名称,方便后续识别。
# (embedding):将嵌入层的输出 embedding 作为 LSTM 层的输入。
lstm = LSTM(units=LSTM_UNITS, dropout=0.2, recurrent_dropout=0.2, name="lstm")(embedding)

# Dense 类:全连接层是神经网络中最基本的层类型,每个输入单元都与每个输出单元相连。
# 1:表示该层只有一个输出单元,通常用于二分类问题。
# activation='sigmoid':使用 sigmoid 激活函数,它会将输出值压缩到 0 到 1 的范围内。在二分类问题中,这个输出值可以看作是属于正类的概率。
# name="output":为该层指定一个名称。
# (lstm):将 LSTM 层的输出 lstm 作为全连接层的输入
output = Dense(1, activation='sigmoid', name="output")(lstm)

#使用 Keras 的函数式 API 构建一个模型,指定输入层为 input_layer,输出层为 output。
model = Model(inputs=input_layer, outputs=output)

# optimizer='adam':使用 Adam 优化器,它是一种自适应学习率的优化算法,能够在训练过程中自动调整学习率。
# loss='binary_crossentropy':使用二元交叉熵损失函数,适用于二分类问题。
# metrics=['accuracy']:在训练和评估过程中,使用准确率作为评估指标。
#标准的模型输入输出
model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])

# 创建各层输出的子模型:embedding_model和lstm_model只是为了便于展示
embedding_model = Model(inputs=input_layer, outputs=embedding)
lstm_model = Model(inputs=input_layer, outputs=lstm)

# ---- 3. 逐层转换演示 ----
print("\n" + "="*50 + "\n逐层数据转换:\n" + "="*50)

# 嵌入层输出
embedding_output = embedding_model.predict(input_data)
print("\n1. 嵌入层输出:")
print(embedding_output)
print("形状:", embedding_output.shape)
print("""
解释:
- 输入: [3 4 1 0 0]
- 每个整数被转换为4维向量:
  - 3(movie) → [?, ?, ?, ?]
  - 4(great) → [?, ?, ?, ?] 
  - 1(<OOV>) → [?, ?, ?, ?]
  - 0(填充) → [0, 0, 0, 0]
- 注意: ?表示随机初始化的值""")

# LSTM层输出
lstm_output = lstm_model.predict(input_data)
print("\n2. LSTM层输出:")
print(lstm_output)
print("形状:", lstm_output.shape)
print("""
解释:
- 输入形状: (1, 5, 4)
- 处理5个时间步的序列:
   t=0: movie向量
   t=1: great向量
   t=2: <OOV>向量
   t=3-4: 零填充
- 输出: 最后一个时间步的3维隐藏状态""")

# 最终输出
final_output = model.predict(input_data)
print("\n3. 最终输出层结果:")
print(final_output)
print("""
解释:
- 输入: LSTM的3维输出
- 经过sigmoid函数转换为概率值
- 当前未经训练,输出是随机值""")

# ---- 4. 模型结构总结 ----
print("\n" + "="*50 + "\n模型结构:\n" + "="*50)
model.summary()

==================================================
输入数据:
==================================================
[[3 4 1 0 0]]
形状: (1, 5)

==================================================
构建模型:
==================================================

==================================================
逐层数据转换:
==================================================
1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 56ms/step


D:\Anaconda\envs\nlp_env\lib\site-packages\keras\src\layers\core\embedding.py:90: UserWarning: Argument `input_length` is deprecated. Just remove it.
  warnings.warn(



1. 嵌入层输出:
[[[ 0.0061442  -0.03541701 -0.01526159 -0.00921419]
  [ 0.01227235  0.01428975 -0.02974085  0.04865975]
  [ 0.00815309  0.01564491  0.00946384 -0.03503773]
  [-0.0486344  -0.03708587 -0.03577558  0.01926471]
  [-0.0486344  -0.03708587 -0.03577558  0.01926471]]]
形状: (1, 5, 4)

解释:
- 输入: [3 4 1 0 0]
- 每个整数被转换为4维向量:
  - 3(movie) → [?, ?, ?, ?]
  - 4(great) → [?, ?, ?, ?] 
  - 1(<OOV>) → [?, ?, ?, ?]
  - 0(填充) → [0, 0, 0, 0]
- 注意: ?表示随机初始化的值
1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 258ms/step

2. LSTM层输出:
[[-0.00810867  0.00977411  0.01125882]]
形状: (1, 3)

解释:
- 输入形状: (1, 5, 4)
- 处理5个时间步的序列:
   t=0: movie向量
   t=1: great向量
   t=2: <OOV>向量
   t=3-4: 零填充
- 输出: 最后一个时间步的3维隐藏状态
WARNING:tensorflow:5 out of the last 9 calls to <function TensorFlowTrainer.make_predict_function.<locals>.one_step_on_data_distributed at 0x000001868C097640> triggered tf.function retracing. Tracing is expensive and the excessive number of tracings could be due to (1) creating @tf.function repeatedly in a loop, (2) passing tensors with different shapes, (3) passing Python objects instead of tensors. For (1), please define your @tf.function outside of the loop. For (2), @tf.function has reduce_retracing=True option that can avoid unnecessary retracing. For (3), please refer to https://www.tensorflow.org/guide/function#controlling_retracing and https://www.tensorflow.org/api_docs/python/tf/function for  more details.
1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 262ms/step

3. 最终输出层结果:
[[0.50071514]]

解释:
- 输入: LSTM的3维输出
- 经过sigmoid函数转换为概率值
- 当前未经训练,输出是随机值

==================================================
模型结构:
==================================================

┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┓
┃ Layer (type)                         ┃ Output Shape                ┃         Param # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━┩
│ input (InputLayer)                   │ (None, 5)                   │               0 │
├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤
│ embedding (Embedding)                │ (None, 5, 4)                │              20 │
├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤
│ lstm (LSTM)                          │ (None, 3)                   │              96 │
├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤
│ output (Dense)                       │ (None, 1)                   │               4 │
└──────────────────────────────────────┴─────────────────────────────┴─────────────────┘



举报

相关推荐

0 条评论