机器学习(一)

一、机器学习基础

1、定义

机器具备有学习的能力。具体来讲就是让机器具备找一个函数的能力。

2、类别

(1)回归(Regression):函数的输出是一个数值,一个标量(scalar);

(2)分类(Classification):让机器做选择题,函数的输出是从设定好的选项里面选择一个当作输出;

(3)结构化学习(Structured Learning):机器产生一个有结构的物体,比如画一张画,写一篇文章。

3、机器学习的训练的三个步骤

(1)写出一个带有未知参数的function;

  • 模型(model):带有位置参数的函数;
  • 特征(feature):函数里面已知的;
  • 权重(weight);
  • 偏置(bias);

(2)定义损失函数lost;(L越大,代表现在这组参数越不好,L越小,代表这组参数越好)

估测值与标签(label)之间的差距计算方法:

  1. 平均绝对误差(MAE):
  2. 均方误差(MSE):
  3. 交叉熵(cross entropy);

误差表面(error surface):尝试了不同的参数,计算它的损失,画出来的等高线图;

(3)最优化(optimization),找到一组最好的(w, b)让损失最小。

常用方法:梯度下降(gredient descent)

  1. 选择一个初始点
  2. 计算**;**
  3. 根据该点的切线斜率判断下一步往哪里走,斜率为正,w向左移,相反右移;
  4. 移动:
    步伐大小取决于:
    · 斜率。斜率大步伐大,斜率小步伐小;
    · 学习率。(超参数:自己设定的)
  5. 重复操作直到失去耐心或

4、线性模型

(1)定义:把输入的特征乘上一个权重再加上一个偏置就得到预测的结果。

(2)模型偏差(model bias):来自于模型的限制。

(3)Hard Sigmoid函数: 当 x 轴的值小于某一个阈值的时候,大于另外一个阈值的时候,中间有一个斜坡。故其图象是先水平,再斜坡,再水平。

(4)分段线性曲线(piecewise linear curve):常数+一组Hard Sigmoid函数的和。

(5)Sigmoid函数:

可以用Sigmoid函数逼近Hard Sigmoid函数,然后把不同的Sigmoid函数叠起来就可以去逼近各种不同的分段线性函数,分段线性函数可以拿来近似各种不同的连续的函数。

如果的值趋近于无穷大的时候,这一项就会消失,当非常大的时候,这一条就会收敛在高度为c的地方。如果负的非常大的时候,分母的地方就会非常大,y的值就会趋近于 0。

(6)不只用一个特征,可以用多个特征代入不同的c,b,w,组合出各种不同的函数,从而得到更有灵活性(flexibility)的函数。

  • 写一个带未知数的function;

用矩阵跟向量相乘的方法表示:

线性代数常用的表示方式:

通过Sigmoid函数计算得到:

即这个比较灵活的函数为:

  • 定义损失;

W 的每一行或者每一列拼成一个长向量,把b ,b"拼"上去,形成一个长向量**** 。损失函数为L()。

  • 优化;

(7)批量(batch):本来有N笔数据,现在B笔数据一组,一组叫做批量。

(8)回合(epoch):把所有批量都看过一次,称为一个回合。

(9)模型变形:Hrad Sigmoid看作两个修正线性单元(ReLU)的加总。ReLU公式如下:

Sigmoid或ReLU被称为激活函数。想要使用ReLU,就把Sigmoid的地方换成

5、深度学习引入

(1)神经元===> 神经网络(neural network);

(2)隐藏层(hidden layer)===>深度学习(deep learning);

二、实践方法论

对训练结果不满意,分析路径:

(1)检查训练集上的loss,如果训练集的loss太大,则说明在训练资料上没有学好,进入第(2)步;如果训练集的loss太小,进入第(3)步,检查测试集上的loss。

(2)训练集上没有学好,有两种可能:通过比较不同模型来判断模型现在到底够不够大。

  • 模型偏差(model bias):模型太过简单,需要重新设计一个模型;
  • 优化做得不好:收敛在局部极限值与鞍点会导致优化失败。
  1. 临界点:梯度为零的点,包括局部极小值(local minimum)、鞍点(saddle point)、局部极大值(local maximum)。

  2. 判断临界值种类的方法:海森矩阵H ;(同时指出了参数可以更新的方向)Hessian 矩阵(海森矩阵)_hessian矩阵-CSDN博客

  3. 批量(batch)大小对梯度下降法的影响:在有并行计算的情况下,小的批量跟大的批量运算的时间并没有太大的差距。除非大的批量非常大,才会显示出差距。但是一个回合需要的时间,小的批量比较长,大的批量反而是比较快的,所以从一个回合需要的时间来看大的批量是较有优势的。而小的批量更新的方向比较有噪声的,大的批量更新的方向比较稳定。但是有噪声的更新方向反而在优化的时候有优势,而且在测试的时候也会有优势。所以大的批量跟小的批量各有优缺点,批量大小是需要去调整的超参数。

  4. 动量(Momentum)法是可以对抗鞍点或局部最小值的方法。可能走到更好的局部最小值。

  5. 自适应学习率:为每一个参数不同的学习率。如果在某一个方向上,梯度的值很小,非常平坦,希望学习率调大一点;如果在某一个方向上非常陡峭,坡度很大,希望学习率可以设得小一点。
    AdaGrad、RMSProp、Adam:最强总结!神经网络中常用的九种优化技术(二)AdaGrad、RMSProp、Adam、学习率衰减、Early Stopping(非常详细)大模型入门到精通!-CSDN博客

  6. 学习率调度:通过动态调整学习率来实现更好的训练效果;
    学习率衰减:随着参数的不断更新,让越来越小;
    预热(Warm Up):让学习率先变大后变小。

  7. 批量归一化(BN):

(3)如果测试集上得loss也小,那么训练是好的,如果测试集上的loss太大,有两种情况:

一是出现了过拟合(overfitting)现象,解决方法:

  1. 增加训练集,可以做数据增强(data augmentation);
  2. 给模型增加一些限制,比如给模型比较少的参数、用较少的特征、早停、正则化、丢弃法等。但是不能给太多限制,否者可能出现模型偏差问题。

交叉验证:合理选择模型的方法

二是训练资料与测试资料不匹配的问题。

三、练习

1、Regression

(1)Dataset:https://www.kaggle.com/competitions/ml2023spring-hw1

(2)解释

  • 随机种子:确保实验的可重复性,计算机的"随机"其实是数学公式生成的伪随机数。种子的作用是固定公式的起点
    为什么要设置"随机种子"_训练模型时需要设置随机种吗-CSDN博客
  • Dtaset:可以通过继承Pytorch提供的Dataset类来构造更适合的数据集类;
    def__init__(self, features, targets = None):读取数据并进行预处理;
    def__getitem__(self, idx):每次提取一笔数据;
    def__len__(self):返回数据的长度;
  • DataLoader:一个迭代器,可以多线程地读取数据,并可将几笔数据组成batch;
    参数列表(待加载数据集,batch大小,是否打乱数据,是否锁页);
    shuffle(是否打乱数据):Traning时设为True,Testing时设为False;
    pin_memory(是否锁页):设置为True时,将数据张量固定在GPU内存中,使数据加载至GPU速度更快;
  • 神经网络模型:通过集成nn.Model类来构造自己的神经网络模型;
    self.layers = nn.Sequential():一个序列容器,将每一layer按照被传入构造器的顺序添加到nn.Sequential()容器中;
    def forward(self, x):前向传播,可理解成神经网络处理输入的过程;
  • 训练过程:对每个batch数据:清零梯度(optimizer.zero_grad()),前向传播计算预测值,计算损失,反向传播(loss.backward()),更新参数(optimizer.step())。
  • 深度学习中有哪些超参数,都有什么作用_实验超参数配置的作用-CSDN博客

(3)代码:

python 复制代码
# 数据操作
import math
import numpy as np

# 读取、写入数据
import pandas as pd
import os
import csv

# 进度条
from tqdm import tqdm

# Pytorch
import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader, random_split

# 绘制图像
from torch.utils.tensorboard import SummaryWriter

'''
    设置随机种子
    在机器学习和深度学习实验中,由于存在随机性(如权重初始化、数据打乱等),每次运行的结果可能会有所不同。
    通过设置固定的随机种子,可以使得每次运行的结果保持一致,便于调试和验证。
'''
def same_seed(seed):
    # 将 cuDNN(CUDA 深度神经网络库)设置为确定性模式,以确保在 GPU 上运行时结果可重复。
    torch.backends.cudnn.deterministic = True
    # 禁用 cuDNN 的自动调优功能,因为该功能会根据硬件选择最优的卷积算法,但这可能导致结果不一致。
    torch.backends.cudnn.benchmark = False

    np.random.seed(seed)
    torch.manual_seed(seed)
    if torch.cuda.is_available():
        torch.cuda.manual_seed(seed)


'''
    划分数据集:本实验从训练集中划分验证集
    data_set:要划分的数据集
    valid_ratio:划分比例
    seed:保证从训练集中随机的划分出验证集
'''
def train_valid_split(data_set, valid_ratio, seed):
    # 计算验证集的大小
    valid_data_size = int(len(data_set) * valid_ratio)
    # 计算训练集的大小
    train_data_size = len(data_set) - valid_data_size
    ''' 
        random_split 函数随机划分数据集:
            按照计算出的训练集和验证集大小进行划分
            使用指定的随机种子确保结果可重现
            generator=torch.Generator().manual_seed(seed) 确保划分过程的随机性是可控的 
    '''
    train_data, valid_data = random_split(data_set, [train_data_size, valid_data_size], generator=torch.Generator().manual_seed(seed))
    return np.array(train_data), np.array(valid_data)


''' 
    选择特征
    最后一列数据是label,x是feature,y是label
    默认选择了全部的特征来做训练,后续可选择合适的特征来优化模型提高预测率
'''
def select_feat(train_data, valid_data, test_data, select_all = True):
    # 分别提取训练数据和验证数据的最后一列作为目标值(即标签)
    y_train = train_data[:, -1]
    y_valid = valid_data[:, -1]

    # 分别提取训练数据、验证数据(除了最后一列)和测试数据的所有特征列
    raw_x_train = train_data[:, :-1]
    raw_x_valid = valid_data[:, :-1]
    raw_x_test = test_data

    if select_all:
        feat_idx = list(range(raw_x_train.shape[1]))
    else:
        feat_idx = [0, 1, 2, 3]

    return raw_x_train[:, feat_idx], raw_x_valid[:, feat_idx], raw_x_test[:, feat_idx], y_train, y_valid


# 数据集
class COVID19Dataset(Dataset):
    '''
        数据初始化方法
        festures:输入特征数据
        targets:目标值(可选,默认为None)
    '''
    def __init__(self, features, targets = None):
        if targets is None:
            self.targets = targets
        else:
            self.targets = torch.FloatTensor(targets)
        self.features = torch.FloatTensor(features)

    '''
        数据获取方法:根据索引返回数据项
        如果没有目标值(如测试集),只返回特征
        如果有目标值(如训练集、验证集),返回 (特征, 目标值) 对
    '''
    def __getitem__(self, index):
        if self.targets is None:
            return self.features[index]
        else:
            return self.features[index], self.targets[index]

    '''
        返回数据集中样本的总数
    '''
    def __len__(self):
        return len(self.features)


# 神经网络,继承自PyTorch的 nn.Module 基类
class My_Model(nn.Module):
    '''
        初始化方法
        input_dim:输入特征的维度
        super(My_Model, self).__init__() 调用父类初始化
        nn.Linear:线性变换层
        nn.ReLU() - ReLU激活函数
    '''
    def __init__(self, input_dim):
        super(My_Model, self).__init__()
        self.layers = nn.Sequential(
            nn.Linear(input_dim, 16),
            nn.ReLU(),
            nn.Linear(16, 8),
            nn.ReLU(),
            nn.Linear(8, 1),
        )

    '''
        通过 self.layers 顺序执行所有定义的层
        使用 x.squeeze(1) 移除张量的第二个维度(索引为1),防止输出出现不必要的维度
    '''
    def forward(self, x):
        x = self.layers(x)
        x = x.squeeze(1)
        return x


# 参数设置
device = 'cuda' if torch.cuda.is_available() else 'cpu'
config = {
    'seed': 5201314,
    'select_all': True,
    'valid_ratio': 0.2,
    'n_epochs': 3000,
    'batch_size': 256,
    'learning_rate': 1e-5,
    'early_stop': 400,
    'save_path': './models/model.ckpt',
}


# 训练过程
def trainer(train_loader, valid_loader, model, config, device):
    # 损失函数和优化器
    criterion = nn.MSELoss(reduction='mean')
    optimizer = torch.optim.SGD(model.parameters(), lr=config['learning_rate'], momentum=0.7)
    writer = SummaryWriter()

    if not os.path.isdir('./models'):
        os.mkdir('./models')

    n_epochs = config['n_epochs']
    best_loss = math.inf
    step = 0
    early_stop_count = 0

    for epoch in range(n_epochs):
        # 设置模型为训练模式
        model.train()
        loss_record = []

        # 使用tqdm进度条显示训练进度
        train_pbar = tqdm(train_loader, position=0, leave=True)

        '''
            对每个batch数据:
            清零梯度(optimizer.zero_grad())
            前向传播计算预测值
            计算损失
            反向传播(loss.backward())
            更新参数(optimizer.step())
        '''
        for x, y in train_pbar:
            optimizer.zero_grad()
            x, y = x.to(device), y.to(device)
            pred = model(x)
            loss = criterion(pred, y)
            loss.backward()
            optimizer.step()
            step += 1
            loss_record.append(loss.detach().item())

            # 显示训练过程
            train_pbar.set_description(f'Epoch[{epoch + 1}/{n_epochs}]')
            train_pbar.set_postfix({'loss': loss.detach().item()})

        mean_train_loss = sum(loss_record) / len(loss_record)
        writer.add_scalar('loss/train', mean_train_loss, step)

        # 设置模型为评估模式
        model.eval()
        loss_record = []
        # 在验证集上计算平均损失,使用torch.no_grad()避免计算梯度以节省内存
        for x, y in valid_loader:
            x, y = x.to(device), y.to(device)
            with torch.no_grad():
                pred = model(x)
                loss = criterion(pred, y)

            loss_record.append(loss.item())

        mean_valid_loss = sum(loss_record) / len(loss_record)
        print(f'Epoch[{epoch + 1}/{n_epochs}]: Train loss : {mean_train_loss: .4f}, Valid loss : {mean_valid_loss: .4f}')
        writer.add_scalar('loss/valid', mean_valid_loss, step)

        # 如果当前验证损失低于历史最佳损失,保存模型
        if mean_valid_loss < best_loss:
            best_loss = mean_valid_loss
            torch.save(model.state_dict(), config['save_path'])
            print('Saving model with loss {: .3f}...'.format(best_loss))
            early_stop_count = 0
        else:
            early_stop_count += 1

        # 连续early_stop轮验证损失未改善则提前停止训练
        if early_stop_count >= config['early_stop']:
            print('\nEarly stopping')
            return



# 设置随机种子
same_seed(config['seed'])
# 读取数据
train_data = pd.read_csv('./covid_train.csv').values
test_data = pd.read_csv('./covid_test.csv').values
# 划分数据集
train_data, valid_data = train_valid_split(train_data, config['valid_ratio'], config['seed'])
print(f"""train_data size: {train_data.shape}, valid_data size: {valid_data.shape}, test_data size: {test_data.shape})""")
# 选择特征
x_train, x_valid, x_test, y_train, y_valid = select_feat(train_data, valid_data, test_data, config['select_all'])
print(f'The number of features: {x_train.shape[1]}')
# 构造数据集
train_dataset = COVID19Dataset(x_train, y_train)
valid_dataset = COVID19Dataset(x_valid, y_valid)
test_dataset = COVID19Dataset(test_data)
# 准备Dataloader
train_loader = DataLoader(train_dataset, batch_size=config['batch_size'], shuffle=True, pin_memory=True)
valid_loader = DataLoader(valid_dataset, batch_size=config['batch_size'], shuffle=True, pin_memory=True)
test_loader = DataLoader(test_dataset, batch_size=config['batch_size'], shuffle=False, pin_memory=True)

# 开始训练
model = My_Model(input_dim=x_train.shape[1]).to(device)
trainer(train_loader, valid_loader, model, config, device)

# 预测
def predict(test_loader, model, device):
    model.eval()
    preds = []
    for x in tqdm(test_loader):
        x = x.to(device)
        with torch.no_grad():
            pred = model(x)
            preds.append(pred.detach().cpu())
    preds = torch.cat(preds, dim=0).numpy()
    return preds


def save_pred(preds, file):
    with open(file, 'w') as fp:
        writer = csv.writer(fp)
        writer.writerow(['id', 'tested_positive'])
        for i, p in enumerate(preds):
            writer.writerow([i, p])


# 预测并保存结果
model = My_Model(input_dim=x_train.shape[1]).to(device)
model.load_state_dict(torch.load(config['save_path']))
preds = predict(test_loader, model, device)
save_pred(preds, 'pred.csv')

2、Classification

(1)Dataset:https://www.kaggle.com/competitions/ml2023spring-hw2

(2)解释

  • 模块化思维:
    BasicBlock:一层Linear层 + ReLu激活函数 = 一个BasicBlock;可以视作封装了一层hidden_layer;
    Classfier:由许多个BasicBlock组成,构成最终要用的神经网络模型。

  • 损失函数nn.CrossEntropyLoss()通常用于多类别分类任务,特别是当类别之间不平衡或样本数目不均衡时;
    交叉熵(Cross-Entropy):一种用于比较两个概率分布之间的度量,对比了模型的预测结果和数据的真实标签,随着预测越来越准确,交叉熵的值越来越小,如果预测完全正确,交叉熵的值就为0。因此,训练分类模型时,可以使用交叉熵作为损失函数。

    一文搞懂熵(Entropy),交叉熵(Cross-Entropy) - 知乎

  • 优化器AdamW(),集成一阶、二阶动量和weight decay正则化的优化器,区别于随机梯度下降法,它能够在训练过程中自适应的调整学习率;

(3)代码:

python 复制代码
import csv
import os

import numpy as np
import torch
import random
from torch.utils.data import Dataset, DataLoader
import torch.nn as nn
from tqdm import tqdm
import gc # 回收内存,节约资源


# 设置随机种子
def same_seeds(seed):
    random.seed(seed)
    np.random.seed(seed)
    torch.manual_seed(seed)
    if torch.cuda.is_available():
        torch.cuda.manual_seed(seed)
        torch.cuda.manual_seed_all(seed)
    torch.backends.cudnn.benchmark = False
    torch.backends.cudnn.deterministic = True


# 读取pt文件(加载特征)
def load_feat(path):
    return torch.load(path)


'''
    拼接frame
    假设我们有以下输入:输入张量 x 形状为 (4, 3),表示4个帧,每帧3个特征,concat_n = 3
    初始输入 x (4个帧,每帧3个特征)
        x = torch.tensor([
            [1, 2, 3],  # 帧0
            [4, 5, 6],  # 帧1
            [7, 8, 9],  # 帧2
            [10, 11, 12] # 帧3
        ])
    1、x.repeat(1, concat_n)
    x = [
        [1, 2, 3, 1, 2, 3, 1, 2, 3],    # 帧0重复
        [4, 5, 6, 4, 5, 6, 4, 5, 6],    # 帧1重复
        [7, 8, 9, 7, 8, 9, 7, 8, 9],    # 帧2重复
        [10, 11, 12, 10, 11, 12, 10, 11, 12] # 帧3重复
    ]
    2、x.view(seq_len, concat_n, feature_dim),重新排列为 (4, 3, 3) 的形状:
    x = [
        # 帧0: [[1,2,3], [1,2,3], [1,2,3]]
        # 帧1: [[4,5,6], [4,5,6], [4,5,6]]
        # 帧2: [[7,8,9], [7,8,9], [7,8,9]]
        # 帧3: [[10,11,12], [10,11,12], [10,11,12]]
    ]
    3、x.permute(1, 0, 2),转置为 (3, 4, 3) 的形状,第一维表示拼接组:
    x = [
        # 组0: [[1,2,3], [4,5,6], [7,8,9], [10,11,12]] - 原始帧
        # 组1: [[1,2,3], [4,5,6], [7,8,9], [10,11,12]] - 重复帧
        # 组2: [[1,2,3], [4,5,6], [7,8,9], [10,11,12]] - 重复帧
    ]
    4、应用 shift 函数
        mid = concat_n // 2 = 1 (中间组索引)
        对于 r_idx = 1:
            x[mid+1, :] = shift(x[mid+1, :], 1) (向右移动1位)
            x[mid-1, :] = shift(x[mid-1, :], -1) (向左移动1位)
        shift(x[mid+1, :], 1) 的执行过程:
            x[2, :] = [[1,2,3], [4,5,6], [7,8,9], [10,11,12]]
            n = 1 > 0,所以:
            left = x[1:] = [[4,5,6], [7,8,9], [10,11,12]]
            right = x[-1].repeat(1, 1) = [[10,11,12]]
            结果:[[4,5,6], [7,8,9], [10,11,12], [10,11,12]]
        shift(x[mid-1, :], -1) 的执行过程:
            x[0, :] = [[1,2,3], [4,5,6], [7,8,9], [10,11,12]]
            n = -1 < 0,所以:
            left = x[0].repeat(1, 1) = [[1,2,3]]
            right = x[:-1] = [[1,2,3], [4,5,6], [7,8,9]]
            结果:[[1,2,3], [1,2,3], [4,5,6], [7,8,9]]
        最终x = [
            # 组0: [[1,2,3], [1,2,3], [4,5,6], [7,8,9]] - 向左移动后的帧
            # 组1: [[1,2,3], [4,5,6], [7,8,9], [10,11,12]] - 原始帧
            # 组2: [[4,5,6], [7,8,9], [10,11,12], [10,11,12]] - 向右移动后的帧
        ]
    5、x.permute(1, 0, 2).view(seq_len, concat_n * feature_dim),重新排列并展平特征维度,得到最终结果:
    [
        [1,2,3, 1,2,3, 4,5,6],      # 帧0: [前1帧, 当前帧, 后1帧]
        [1,2,3, 4,5,6, 7,8,9],      # 帧1: [前1帧, 当前帧, 后1帧]
        [4,5,6, 7,8,9, 10,11,12],   # 帧2: [前1帧, 当前帧, 后1帧]
        [7,8,9, 10,11,12, 10,11,12] # 帧3: [前1帧, 当前帧, 后1帧]
    ]
'''

# shift 函数用于对输入张量 x 进行移位操作
def shift(x, n):
    if n < 0:
        left = x[0].repeat(-n, 1)
        right = x[:n]
    elif n > 0:
        left = x[n:]
        right = x[-1].repeat(n, 1)
    else:
        return x
    return torch.cat((left, right), dim=0)



'''
    concat_feat 函数用于将特征帧进行拼接以增加上下文信息:
    这个函数的主要目的是为每个时间帧添加前后帧的上下文信息, 以帮助模型更好地理解语音信号的时序特征。
'''
def concat_feat(x, concat_n):
    assert concat_n % 2 == 1
    if concat_n < 2:
        return x

    seq_len = x.size(0)     # frame的个数
    feature_dim = x.size(1)     # 每一个frame的维度
    x = x.repeat(1, concat_n)
    # 重新排列张量形状为 (concat_n, seq_len, feature_dim),即[组号, 帧序号, 帧内号]
    x = x.view(seq_len, concat_n, feature_dim).permute(1, 0, 2)

    mid = concat_n // 2

    # 对于中间帧周围的每一帧: 使用 shift 函数对称地移动帧以创建上下文
    for r_idx in range(1, mid+1):
        x[mid+r_idx, :] = shift(x[mid+r_idx], r_idx)
        x[mid-r_idx, :] = shift(x[mid-r_idx], -r_idx)
    # 最后将张量重新排列并展平特征维度, 得到拼接后的特征
    return x.permute(1, 0, 2).view(seq_len, concat_n * feature_dim)


# 预处理数据
def preprocess_data(split, feat_dir, phone_path, concat_nframes, train_ratio=0.8):
    class_num = 41  # 41个分类
    mode = 'train' if (split == 'train' or split == 'val') else 'test'

    # 将每个文件名及其对应的标签序列存储在 label_dick 字典中
    label_dick = {}
    if mode != 'test':
        phone_file = open(os.path.join(phone_path, f'{mode}_labels.txt')).readlines()
        for line in phone_file:
            line  = line.strip('\n').split(' ')
            # temp = []
            # for p in line[1:]:
            #     temp.append(int(p))
            label_dick[line[0]] = [int(p) for p in line[1:]]

        # 划分数据集
        if split == 'train' or split == 'val':
            usage_list = open(os.path.join(phone_path, 'train_split.txt')).readlines()
            random.shuffle(usage_list)
            percent = int(len(usage_list) * train_ratio)
            if split == 'train':
                usage_list = usage_list[:percent]
            elif split == 'val':
                usage_list = usage_list[percent:]
    elif split == 'test':
            usage_list = open(os.path.join(phone_path, 'test_split.txt')).readlines()
    else:
        raise ValueError('Invalid \'split\' argument for dataset: PhoneDataset!')

    usage_list = [line.strip('\n') for line in usage_list]
    print('[Dataset] - # phone classes: {}, # samples: {:d}'.format(class_num, len(usage_list)))

    # 创建预分配的张量 x 和 y 用于存储特征和标签
    max_len = 10000000
    x = torch.empty(max_len, 39 * concat_nframes)
    if mode != 'test':
        y = torch.empty(max_len, dtype=torch.long)
    '''
        遍历文件列表,将特征和标签存储到预分配的张量中
        使用 torch.empty 预分配内存以提高效率
        通过 concat_feat 函数增加时序上下文信息
        使用 tqdm 显示处理进度
        最终通过切片操作 x[:idx, :] 和 y[:idx] 去除预分配的多余空间
    '''
    idx = 0
    for i, fname in tqdm(enumerate(usage_list)):
        feat = load_feat(os.path.join(feat_dir, mode, f'{fname}.pt'))
        cur_len = len(feat)
        feat = concat_feat(feat, concat_nframes)
        if mode != 'test':
            label = torch.LongTensor(label_dick[fname])
        x[idx: idx + cur_len] = feat
        if mode != 'test':
            y[idx: idx + cur_len] = label

        idx += cur_len

    x = x[:idx, :]
    if mode != 'test':
        y = y[:idx]
    print(f'[INFO]{split} set')
    print(x.shape)
    if mode != 'test':
        print(y.shape)
        return x, y
    else:
        return x


# Dataset
class LibriDataset(Dataset):
    def __init__(self, x, y=None):
        self.feature = x
        if y is not None:
            self.label = torch.LongTensor(y)
        else:
            self.label = None

    def __getitem__(self, item):
        if self.label is not None:
            return self.feature[item], self.label[item]
        else:
            return self.feature[item]

    def __len__(self):
        return len(self.feature)


# Model
class BasicBlock(nn.Module):
    def __init__(self, input_dim, output_dim):
        super(BasicBlock, self).__init__()
        self.block = nn.Sequential(
            nn.Linear(input_dim, output_dim),
            nn.ReLU(),
        )

    def forward(self, x):
        x = self.block(x)
        return x


class Classifier(nn.Module):
    def __init__(self, input_dim, output_dim=41, hidden_layers=1, hidden_dim=256):
        super(Classifier, self).__init__()
        self.fc = nn.Sequential(
            BasicBlock(input_dim, hidden_dim),      # 输入层
            *[BasicBlock(hidden_dim, hidden_dim) for _ in range(hidden_layers)],    #hidden_layers 个 BasicBlock 作为隐藏层
            nn.Linear(hidden_dim, output_dim)   # 一个线性层将隐藏层映射到输出维度
        )

    def forward(self, x):
        x = self.fc(x)
        return x

# 配置
device = 'cuda' if torch.cuda.is_available() else 'cpu'
concat_nframes = 3
train_ratio = 0.8
config = {
    'seed': 0,
    'batch_size': 512,
    'n_epoch': 5,
    'learning_rate': 1e-4,
    'model_path': './model.ckpt'
}
input_dim = 39 * concat_nframes
hidden_layers = 3
hidden_dim = 256

# 训练
def trainer(train_set, valid_set, train_loader, val_loader, config, model, device):
    # 损失函数和优化器
    criterion = nn.CrossEntropyLoss()
    optimizer = torch.optim.AdamW(model.parameters(), lr=config['learning_rate'])

    best_acc = 0.0
    n_epochs = config['n_epoch']

    for epoch in range(n_epochs):
        train_acc = 0.0
        train_loss = 0.0
        model.train()
        for i, batch in enumerate(tqdm(train_loader)):
            features, labels = batch
            features = features.to(device)
            labels = labels.to(device)

            optimizer.zero_grad()
            outputs = model(features)
            loss = criterion(outputs, labels)
            loss.backward()
            optimizer.step()

            _, train_pred = torch.max(outputs, dim = 1)
            train_acc += (train_pred.detach() == labels.detach()).sum().item()
            train_loss += loss.item()

            #
            if (len(valid_set)) > 0:
                val_acc = 0.0
                val_loss = 0.0
                model.eval()
                with torch.no_grad():
                    for i, batch in enumerate(tqdm(val_loader)):
                        features, labels = batch
                        features = features.to(device)
                        labels = labels.to(device)
                        outputs = model(features)
                        loss = criterion(outputs, labels)

                        _, val_pred = torch.max(outputs, dim=1)
                        val_acc += (val_pred.cpu() == labels.cpu()).sum().item()
                        val_loss += loss.item()

                    print(
                        '[Epoch {:03d}/{:03d}] train_loss: {:.4f}, train_acc: {:.3f}, val_loss: {:.4f}, val_acc: {:.3f}'.format(
                            epoch + 1, n_epochs,
                            train_loss / len(train_loader), train_acc / len(train_set),
                            val_loss / len(val_loader), val_acc / len(valid_set)))

                    # 保存最优模型
                    if val_acc > best_acc:
                        best_acc = val_acc
                        torch.save(model.state_dict(), config['model_path'])
                        print('saving model with acc {:.3f}'.format(best_acc))
            else:
                print('[Epoch {:03d}/{:03d}] train_loss: {:.4f}, train_acc: {:.3f}'.format(
                    epoch + 1, n_epochs,
                    train_loss / len(train_loader), train_acc / len(train_set)))

    if len(valid_set) == 0:
        torch.save(model.state_dict(), config['model_path'])
        print('saving model with acc {:.3f}'.format(best_acc))

    del train_loader, val_loader
    gc.collect()
    return

# same_seeds(config['seed'])
# train_x, train_y = preprocess_data(split='train', feat_dir='./dataset/feature', phone_path='./dataset', concat_nframes=concat_nframes, train_ratio=train_ratio)
# val_x, val_y = preprocess_data(split='val', feat_dir='./dataset/feature', phone_path='./dataset', concat_nframes=concat_nframes, train_ratio=train_ratio)
#
# train_set = LibriDataset(train_x, train_y)
# val_set = LibriDataset(val_x, val_y)
#
# del train_x, train_y, val_x, val_y
# gc.collect()
#
# train_loader = DataLoader(train_set, batch_size=config['batch_size'], shuffle=True, pin_memory=True)
# val_loader = DataLoader(val_set, batch_size=config['batch_size'], shuffle=True, pin_memory=True)
#
# print('==> Building model and loading data ...')
# model = Classifier(input_dim=input_dim, hidden_layers=hidden_layers, hidden_dim=hidden_dim).to(device)
# trainer(train_set, val_set, train_loader, val_loader, config, model, device)


# Predict
def predict(model, test_loader):
    # test_acc = 0.0
    # test_length = 0
    pred = np.array( [], dtype=np.int32)

    model.eval()
    with torch.no_grad():
        for i, batch in enumerate(tqdm(test_loader)):
            features = batch
            features = features.to(device)

            outputs = model(features)
            _, test_pred = torch.max(outputs, dim=1)
            pred = np.concatenate( (pred, test_pred.cpu().numpy()), axis=0 )

        return  pred

# def save_pred(preds, file):
#     with open(file, 'w') as fp:
#         fp.write('Id Class\n')
#         for i, p in enumerate(preds):
#             fp.write('{} {}\n'.format(i, p))

def save_pred(preds, file):
    with open(file, 'w') as fp:
        writer = csv.writer(fp)
        writer.writerow(['Id', 'Class'])
        for i, p in enumerate(preds):
            writer.writerow([i, p])


test_x = preprocess_data(split='test', feat_dir='./dataset/feature', phone_path='./dataset', concat_nframes=concat_nframes)
test_set = LibriDataset(test_x, None)
test_loader = DataLoader(test_set, batch_size=config['batch_size'], shuffle=False, drop_last=False, pin_memory=True)
model = Classifier(input_dim=input_dim, hidden_layers=hidden_layers, hidden_dim=hidden_dim).to(device)
model.load_state_dict(torch.load(config['model_path']))

preds = predict(model, test_loader)
save_pred(preds, 'prediction.csv')
相关推荐
AngelPP3 小时前
OpenClaw 架构深度解析:如何把 AI 助手搬到你的个人设备上
人工智能
宅小年3 小时前
Claude Code 换成了Kimi K2.5后,我再也回不去了
人工智能·ai编程·claude
九狼3 小时前
Flutter URL Scheme 跨平台跳转
人工智能·flutter·github
ZFSS3 小时前
Kimi Chat Completion API 申请及使用
前端·人工智能
天翼云开发者社区4 小时前
春节复工福利就位!天翼云息壤2500万Tokens免费送,全品类大模型一键畅玩!
人工智能·算力服务·息壤
知识浅谈4 小时前
教你如何用 Gemini 将课本图片一键转为精美 PPT
人工智能
Ray Liang5 小时前
被低估的量化版模型,小身材也能干大事
人工智能·ai·ai助手·mindx
shengjk16 小时前
NanoClaw 深度剖析:一个"AI 原生"架构的个人助手是如何运转的?
人工智能
西门老铁8 小时前
🦞OpenClaw 让 MacMini 脱销了,而我拿出了6年陈的安卓机
人工智能