基于pytorch的steam游戏评分的线性回归问题分析

请添加图片描述

前言

  • 相信已经暑假一个月的大家肯定并不陌生上面这个学习软件(),面对琳琅满目的游戏总是让人不知道挑选什么,这时候一个游戏的评分往往便成为了一个玩家选择下载的原因,那么今天我们就来研究研究,steam上一个游戏的种种数据,是如何影响到其最终评分。
  • 本文将使用pytorch对steam_datasets数据集进行线性回归分析,并在文章结尾尝试加入非线性优化模型。
  • 本文仅提供处理此问题的一种思路,并不是最优,如有错误,请多多指正。
  • 数据集链接

问题阐述

  • 以下是数据集steam_datasets.csv的一小部分截图,它包含以下几个信息
    请添加图片描述

    • Column1:行数
    • game:游戏名
    • release:发行日期(1)
    • peak_players:巅峰玩家数 (2)
    • positive_reviews:好评(3)
    • negative_reviews:差评(4)
    • total_reviews: 总评价数
    • primary_genre: 游戏分类 (5)
    • detected_technologies 游戏开发平台(6)
    • review_percentage:玩家对游戏的总体满意程度(7)
    • players_right_now:当前玩家数(8)
    • 24_hour_peak:24小时玩家巅峰数(9)
    • all_time_peak:总玩家巅峰数(10)
    • rating:游戏打分(输出)
  • 要求根据上述提供的数据集完成下述要求:

    1. 标签选取:以上述10个标签作为线性回归的输入,尝试对rating进行线性回归预测
    2. 读取数据集:自行从csv数据集挑选合适数量的数据集(请不要全拿进去)作为训练集(注意数据预处理和错误数据的剔除)
    3. 选择合适的模型及其优化算法对模型进行训练
    4. 要求使用pytorch实现上述代码

数据预处理

数据解码
  • 在拿到数据集steam_datasets.csv并对其进行回归处理之前,我们首先需要对数据进行读取,在对数据进行读取之前,我们可以使用chardet库(这是一个用于检测文本数据的字符编码的库),对数据集进行编码检测,从而在使用pandas对数据集读取时指定正确的编码格式。
import chardet  
with open('./steam_datasets.csv', 'rb') as f:  
    result = chardet.detect(f.read())  # 编码检测  
print(result['encoding'])  # 输出编码格式
  • 此外,GB2312GBKGB18030,着三个编码格式是兼容的,包含的字符个数:GB2312 <GBK< GB18030,这里chardet库给出的结果是GB2312,但是使用这个编码格式进行读取会遇到报错,故我们往上选择GBK进行数据解码。

数据读取
  • Pandas 是一个开放源码、BSD 许可的库,提供高性能、易于使用的数据结构和数据分析工具。这里我们使用Pandascsv数据集进行读取,并指定编码格式。
import pandas as pd
raw_data=pd.read_csv('./steam_datasets.csv',encoding="GBK")
print(raw_data)

请添加图片描述


异常数据清理
  • 在完成数据转换之后,我们需要进行数据清洗,这包括处理缺失值、异常值和不必要的列。
  1. 处理缺失值: 对于缺失值,可以选择填充、删除含有缺失值的行或者使用模型预测缺失值。这里我们直接丢失。
raw_data = raw_data.dropna()
  1. 删除不必要的列:对于不是10标签输入的列,我们选择删除。
raw_data = raw_data.drop(['game','Column1','total_reviews'], axis=1)
raw_data = raw_data.dropna()
  1. 剔除非数值数据:对于标签peak_players, positive_reviews, negative_reviews, review_percentage, players_right_now, 24_hour_peak, all_time_peak, rating这8列都应该是数据类型,我们观察csv文件可以发现在这些标签中,混杂着非数值类型的日期和字符串,为此我们需要进行删除。
    • 如下出现了ratingall_time_peak出现非法值的情况请添加图片描述
def to_numeric_if_possible(x):  
    try:  
        return pd.to_numeric(x)  
    except (ValueError, TypeError):  
        return pd.NA  
check_labels=['peak_players', 'positive_reviews', 'negative_reviews', 'review_percentage', 'players_right_now', '24_hour_peak', 'all_time_peak', 'rating']  
# 指定列名并应用自定义函数  
raw_data[check_labels] = raw_data[check_labels].applymap(to_numeric_if_possible)  
  
raw_data = raw_data.dropna()

数据分析–非数据类型标签转换
  • 通过观察上述10标签中,我们注意到release,primary_genre,detected_technologies着三个标签属于非数值,对于非数值类型的标签,我们需要转换为数值类型的标签来进行线性回归。
日期类数据处理
  • 观察release2023/1/26,属于日期类数据类型,我们可以将日期转换为距离某个特定日期的天数,这样可以作为一个连续变量用于回归。
  • 这里需要注意数据集中包含无法解析的日期类型,需要当作异常数据剔除。
from datetime import datetime  
# 尝试将 'release' 列转换为日期,无法解析的设置为 NaT
raw_data['release'] = pd.to_datetime(raw_data['release'], errors='coerce', format='%Y/%m/%d')  
  
# 删除包含 NaT 的行  
raw_data = raw_data.dropna(subset=['release'])  
# 选择2005-01-01作为参考点
specific_date = datetime.strptime('2005-01-01', '%Y-%m-%d')  
raw_data['release'] = pd.to_datetime(raw_data['release']).map(lambda date: (date - specific_date).days)  
print(raw_data['release'])  
# 计算依次最大最小值是为了检查是否出现非法值,此外根据最大最小值调整参考日期
print(max(raw_data['release']))  
print(min(raw_data['release']))

字符型数据处理
  • 这里先介绍几种常见的非数值类型编码方式:
    • 哑编码(独热编码,One-Hot Encoding): 独热编码是一种将分类变量转换为一系列二进制列的过程,其中每列对应一个可能的类别值。这些列中的值通常为0或1,表示某个特定类别是否存在。
      • 当分类变量的类别数量较少时,独热编码是一种有效的处理方法。
    • 分解(Factorization):分解是将分类数据转换为整数的过程。每个类别被分配一个唯一的整数。这种方法不会产生独热编码那样的稀疏矩阵,但它可能会丢失一些类别间的信息,因为它只保留了一个整数而不是整个类别结构。
      • 当类别数量较多,且不需要保留原始类别结构时,分解是一种节省空间的处理方法。
    • 标签编码(Label Encoding):类别编码是将类别转换为数值的方法
  • 那么我们现在来观察这两个字符型数据,首先来观察primary_genre
    • 请添加图片描述

    • 观察可以发现,每个游戏分类都已经分配好对应的序号,且继续观察多标签的行数基本上都属于错误数据,都已经被剔除,故这里只需要提取出单分类的游戏分类对应的数据即可

  • 我们导入正则表达式re库对括号的数字进行提取,并对其中是否为单数字进行检测
import re  
# 使用正则表达式提取括号中的数字,并将它们转换为逗号分隔的字符串  
raw_data['primary_genre'] = raw_data['primary_genre'].apply(  
    lambda x: ','.join(re.findall(r'\((\d+)\)', x)))  
  
is_single_digit = raw_data['primary_genre'].apply(lambda x: all(len(str(item)) == 1 for item in x))  
# 检查是否全部为单数字  
all_single_digit = is_single_digit.all()  
print(all_single_digit)  
print(raw_data['primary_genre'])  
# 将字符串转换为数字,使用之前的函数
raw_data['primary_genre'] = raw_data['primary_genre'].apply(to_numeric_if_possible)  
raw_data = raw_data.dropna()  
  
print(raw_data['primary_genre'])  
# 输出最大最小值进行确认
print(max(raw_data['primary_genre']))  
print(min(raw_data['primary_genre']))

  • 然后接着我们来看detected_technologies
  • 观察发现,detected_technologies由好几种开发工具组成,为此我们使用代码进行统计请添加图片描述
primary_genre_str = raw_data['detected_technologies'].str.cat(sep=';')  
  
# split 分割文本  
entries = [entry for line in primary_genre_str.split("\n") for entry in line.split(";")]  
all_categories=set()  
for entry in entries:  
    entry = entry.strip()  # 移除前后的空白字符  
    if '.' in entry:  
        all_categories.add(entry.split('.')[0])  
print(all_categories)
  • 通过统计字符.前面的字符串,我们得到了所有字符的分类请添加图片描述

  • 这里我们对所有标签进行统计,计算总类别数

all_categories=set()  
# 遍历每个条目,检查是否以 all_categories_label 中的某个标签开头  
for entry in entries:  
    entry = entry.strip()  
    for label in all_categories_label:  
        if entry.startswith(label):  
            all_categories.add(entry)  
            break  
print("all_categories ", all_categories)  
print("Number of all_categories:", len(all_categories))
  • 一共151类,够多的…这么看来使用独热编码是不太现实的,这里我采用标签编码 请添加图片描述
# 进行标签编码  
all_categories_dict = {label: idx for idx, label in enumerate(sorted(all_categories))}  
print(all_categories_dict)
  • 如下顾名思义请添加图片描述

  • 紧接着我们对原数据中的标签进行替换

# 使用正则表达式检查是否为数字  
data_as_lists = []  
for entry in raw_data['detected_technologies']:  
    # 分割字符串  
    split_entries = entry.split('; ')  
    # 只保留数字部分  
    numeric_entries = [int(num) for num in split_entries if re.match(r'^\d+$', num)]  
    data_as_lists.append(numeric_entries)  
raw_data['detected_technologies']=data_as_lists  
  
print(raw_data['detected_technologies'])
  • 原始数据detected_technologies就完成了非字符型的处理的第一步请添加图片描述

  • 仔细观察,上述detected_technologies的数据的每一行是一个长度不定的输入,对于线性模型,在不想增加其输入维度的情况下,这里我采用PCA降维

  • 主成分分析(PCA,Principal Component Analysis)是一种统计方法,它通过正交变换将一组可能相关的变量转换为一组线性不相关的变量,这组变量称为主成分。PCA的主要目的是降维,即在尽可能保留原始数据信息的前提下,减少数据的特征维度。

from sklearn.decomposition import PCA  
import numpy as np  
  
# 找到最长的列表长度  
max_length = max(len(lst) for lst in raw_data['detected_technologies'])  
  
# 使用列表推导式和列表的extend方法来填充列表,确保所有列表长度一致  
padded_technologies = [x + [0]*(max_length - len(x)) for x in raw_data['detected_technologies']]  
  
technologies_array = np.array(padded_technologies)  
# 应用PCA降维到1维  
pca = PCA(n_components=1)  
technologies_pca = pca.fit_transform(technologies_array)  
print(technologies_pca)  
raw_data['detected_technologies']=technologies_pca
  • 降维后我们得到:请添加图片描述

最终检查
  • 最终再确认一下
all_label=['rating','release','peak_players','positive_reviews','negative_reviews','primary_genre','detected_technologies','review_percentage','players_right_now','24_hour_peak','all_time_peak']  
# 再次检查每一列的数据类型  
for col in all_label:  
    print(f"{col}: {raw_data[col].dtype}")  
# 确保所有列都是数值类型  
for col in all_label:  
    if col=='detected_technologies':  
        continue  
    if raw_data[col].dtype == object:  
        # 尝试将非数值类型转换为数值类型  
        raw_data[col] = pd.to_numeric(raw_data[col], errors='coerce')  
  
# 删除任何仍然包含 NaN 值的行  
raw_data = raw_data.dropna(subset=all_label)  
# 再次检查每一列的数据类型  
for col in all_label:  
    print(f"{col}: {raw_data[col].dtype}")  
  
print(raw_data)


模型训练

  • 那么我们正式开始训练模型…
生成训练集/测试集
  • 数据标准化(Standardization):数据标准化旨在调整数据的尺度,使每个特征具有相同的数值范围,从而消除特征之间的量纲影响,确保数据在训练过程中被平等对待
  • 训练集(Training Set)-用于训练模型,即通过迭代优化模型的参数(如权重和偏置)来最小化损失函数。(有时候还会进行训练集验证集的划分)
  • 测试集(Test Set): 用于评估模型在未见过的数据上的表现,即模型的泛化能力。
import torch  
from sklearn.model_selection import train_test_split  
import torch.nn as nn  
from sklearn.preprocessing import StandardScaler  
  
# 选择除了 'rating' 之外的所有列作为特征  
X = raw_data.drop(columns=['rating']).values  
# 标准化
scaler = StandardScaler()  
X = scaler.fit_transform(X)  
y = raw_data['rating'].values  
  
# 划分训练集和测试集  
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)   
# 将数据转换为PyTorch张量  
X_train_tensor = torch.tensor(X_train, dtype=torch.float32)  
y_train_tensor = torch.tensor(y_train, dtype=torch.float32).view(-1, 1)  
X_test_tensor = torch.tensor(X_test, dtype=torch.float32)  
y_test_tensor = torch.tensor(y_test, dtype=torch.float32).view(-1, 1)

定义模型
  • 这边定义了一个简单的线性模型,继承自nn模组,需要重写forward用于前向传播
class LinearRegressionModel(nn.Module):  
    def __init__(self, input_dim):  
        super(LinearRegressionModel, self).__init__()  
        self.linear = nn.Linear(input_dim, 1)  
  
    def forward(self, x):  
        return self.linear(x)  
  
# 实例化模型  
input_dim = X_train.shape[1]  
model = LinearRegressionModel(input_dim)

定义损失函数和优化器
  • 均方误差损失(MSELoss)
    • MSELoss计算预测值和真实值之间差的平方的平均值。
    • 它适用于回归问题,特别是当输出是连续值时。
    • 数学表达式为:MSELoss=1𝑛∑𝑖=1𝑛(𝑦𝑖−𝑦𝑖)2MSELoss=n1​∑i=1n​(yi​−y​i​)2 其中,𝑦𝑖yi​ 是真实值,𝑦𝑖y​i​ 是预测值,𝑛n 是样本数量。
  • 随机梯度下降(SGD)
    • SGD是一种常见的优化算法,用于寻找使损失函数最小化的参数。
    • model.parameters() 是模型中所有可学习参数的迭代器。
    • lr 是学习率(learning rate),它控制了参数更新的步长大小。学习率的选择对模型训练至关重要,太低的学习率可能导致训练缓慢,而太高的学习率可能导致训练过程不稳定或越过最小值。
criterion = nn.MSELoss()  
optimizer = torch.optim.SGD(model.parameters(), lr=0.004)

模型训练
  • 模型的训练很简单,如下,需要记得手动进行梯度清零和设置反向传播优化即可
# 训练模型  
num_epochs = 1000  
for epoch in range(num_epochs):  
    model.train()  
    optimizer.zero_grad()  
    # 前向传播  
    outputs = model(X_train_tensor)  
    # 计算损失  
    loss = criterion(outputs, y_train_tensor)  
    # 反向传播和优化  
    loss.backward()  
    optimizer.step()  
  
    if (epoch+1) % 100 == 0:  
        print(f'Epoch [{epoch+1}/{num_epochs}], Loss: {loss.item():.4f}')

模型测试
  • 在测试集上进行测试
# 在测试集上验证模型  
model.eval()  
with torch.no_grad():  
    predictions = model(X_test_tensor)  
    test_loss = criterion(predictions, y_test_tensor)  
    print(f'Test Loss: {test_loss.item():}')

获取结果
  • 通过输出权重和偏差,我们可以得到我们的方程
weights = model.linear.weight.data  
bias = model.linear.bias.data  
print(f"Weights: {weights}")  
print(f"Bias: {bias}")
  • 如下我们得到10输入的权重和偏差值请添加图片描述

  • 通过上述步骤,我们观察到损失在逐渐减小最后稳定请添加图片描述


总结和优化-加入非线性

加入ReLu激活函数和BN
  • 可以看得出来,上述线性模型的效果并没用达到很好的效果,这时我们考虑引入非线性
class ComplexModel(nn.Module):  
    def __init__(self, input_size):  
        super(ComplexModel, self).__init__()  
        self.fc = nn.Linear(input_size, 64)  
        self.bn = nn.BatchNorm1d(64)  
        self.relu = nn.ReLU()  
        self.fc2 = nn.Linear(64, 1)  
  
    def forward(self, x):  
        out = self.fc(x)  
        out = self.bn(out)  
        out = self.relu(out)  
        out = self.fc2(out)  
        return out
  • 批量归一化(Batch Normalization,简称BN)是一种用于加速深度网络训练的技术,同时也可以作为一种正则化手段,提高模型的泛化能力。
  • ReLU(Rectified Linear Unit)激活函数是目前深度学习中使用最广泛的激活函数之一
    • 𝑓(𝑥)=max⁡(0,𝑥)f(x)=max(0,x)
    • 非线性:ReLU函数为网络引入了非线性特性,这对于神经网络能够捕捉复杂的数据模式至关重要。
更换优化器Adam
  • 同时我们更换优化器为Adam:Adam算法通过计算梯度的一阶矩估计(即均值)和二阶矩估计(即未中心化的方差)来适应性地调整每个参数的学习率。
optimizer = torch.optim.Adam(model.parameters(), lr=0.4)
增加迭代轮数
  • 我们稍微增加以下迭代轮数
num_epochs = 50000
结果
  • 通过训练结果可以看到,相比于线性模型,非线性的引入使模型的loss下降了,虽然还有一些参数和模型可以调整进一步优化(懒了不想做啦)
    请添加图片描述

总结

  • 从上述我们可以看出,数据预处理实际上远远比模型训练更重要也更关键,往往成败就在数据处理种
  • 非线性的引入会使模型损失大大减小
  • 本文仅提供处理此问题的一种思路,并不是最优,还有参数可以调整,如有错误,请多多指正。
  • 17
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值