动机
因为看到文献说神经网络(这里指多层线性层 + 激活函数)能够实现非线性的运算从而比简单的单层输入加单层输出网络全面,虽然在脑海中能有大致的拟合思路。
将输入的两个数通过隐含层进行拆分二进制,从而进行异或操作,但是实践出真知,因此打算自己来写一下试一下。
思路
- 一开始,我的模型设计就是输入两个数字的输入层,一个隐藏层,在一个输出异或结果的输出层,具体如下(Linear(2,32),Relu,Linear(32,32),Relu,Linear(32,1))但是经过实际运行,效果超级不好。
- 原因细想一下很简单,因为异或操作的本质是位运算,我这样子设计网络的原因比较粗暴,我希望神经网络在输入层的时候将两个数都拆解成16位的二进制数,然后在隐藏层实现异或操作,最后在输出层输出异或结果,但是太理想化,没有良好的loss函数约束,神经网络很难学到这样子复杂的位运算操作。(因为这里我图省事直接用了mse的loss,神经网络很难通过两个数之间的差值来理解位运算。)
- 后来我就想明白了,我直接把输入的两个数直接拆解成了16 + 16 位的二进制数,网络重新设计成了(Linear(32,64),Relu,Linear(64,64),Relu,Linear(64,16),Sigmoid)。同时损失函数我使用了Bce来进行约束,这样任务实际上就变成了用两个数的二进制表示来预测他们的异或结果的二进制表示,并且使用Bce可以正确的指导神经网络来拟合异或操作。结果也很好,代码如下。
代码
生成数据集的代码(c++)
代码图省事乱写的,能用就行。得到生成的数据集文件train.txt,val.txt,test.txt
#include "bits/stdc++.h"
using namespace std;
map<pair<int, int>, bool> m;
int main() {
//写入文件
string train_file = "train.txt";
string val_file = "val.txt";
string test_file = "test.txt";
int train_num = 40000;
int val_num = 10000;
int test_num = 10000;
//生成训练集 随机两个数的异或
freopen(train_file.c_str(), "w", stdout);
srand(time(0));
for (int i = 0; i < train_num; i += 1) {
//随机生成1 - 30000 之间的两个数
int a = rand() % 30000 + 1;
int b = rand() % 30000 + 1;
pair<int, int> p = make_pair(a, b);
if (m[p]) {
i--;
continue;
}
cout << a << " " << b << " " << (a ^ b) << endl;
}
//生成验证集
freopen(val_file.c_str(), "w", stdout);
srand(time(0));
for (int i = 0; i < val_num; i += 1) {
//随机生成1 - 30000 之间的两个数
int a = rand() % 30000 + 1;
int b = rand() % 30000 + 1;
pair<int, int> p = make_pair(a, b);
if (m[p]) {
i--;
continue;
}
cout << a << " " << b << " " << (a ^ b) << endl;
}
//生成测试集
freopen(test_file.c_str(), "w", stdout);
srand(time(0));
for (int i = 0; i < test_num; i += 1) {
//随机生成1 - 30000 之间的两个数
int a = rand() % 30000 + 1;
int b = rand() % 30000 + 1;
pair<int, int> p = make_pair(a, b);
if (m[p]) {
i--;
continue;
}
cout << a << " " << b << " " << (a ^ b) << endl;
}
return 0;
}
数据集生成结果
pytorch dataset 文件
也很简单,就是读入一下转成二进制的数组表示就ok了
import torch
from torch.utils.data import Dataset
class xor(Dataset):
def __init__(self, split='train',bit_dim=16):
self.data = []
with open(f'./dataset_file/xor/{split}.txt', 'r') as f:
for line in f:
# 假设数据格式为 a,b,c
parts = line.strip().split(' ')
a = float(parts[0])
b = float(parts[1])
c = float(parts[2])
# 将数据转换为bit_dim位二进制
a = [int(x) for x in bin(int(a))[2:]]
b = [int(x) for x in bin(int(b))[2:]]
c = [int(x) for x in bin(int(c))[2:]]
while len(a) < bit_dim:
a = [0] + a
while len(b) < bit_dim:
b = [0] + b
while len(c) < bit_dim:
c = [0] + c
x = a + b
self.data.append((x, c))
def __len__(self):
return len(self.data)
def __getitem__(self, idx):
x, c = self.data[idx]
return torch.tensor(x), torch.tensor(c)
模型设计
这里为什么要把隐藏层的层数设为64呢
具体的可以看这篇 多层感知机是如何解决异或问题的? - 鸽我哥哥的回答 - 知乎
https://www.zhihu.com/question/263676843/answer/1549135982
按照上面这个回答的原理,对于两个1位的异或问题,需要2维度的隐含层,那么对于我们这个问题而言,就相当于是两个16位的异或问题,至少需要32维度的隐含层,但是经过我的实际测试,在隐含层维度位32维的时候,能预测,但是预测的不准,通过增加隐含层的维度后,效果显著提升(至少升到40维)。因此为了凑2进制的数,我把隐含层的维度设计成了64维,其实在网上加也一样,因为到后来神经网络已经完全拟合了异或的操作,训练集,验证集,测试集全对了。
import torch
import torch.nn as nn
class xorModel(nn.Module):
def __init__(self):
super(xorModel, self).__init__()
self.fc1 = nn.Linear(32, 64)
self.xor = nn.Linear(64, 64)
self.output = nn.Linear(64, 16)
self.relu = nn.LeakyReLU()
self.sigmoid = nn.Sigmoid()
# self.output = nn.Linear(16, 1)
def forward(self, x):
x = x.flatten(start_dim=1)
x = self.fc1(x)
x = self.relu(x)
x = self.xor(x)
x = self.relu(x)
x = self.output(x)
x = self.sigmoid(x)
return x
其他一些杂七杂八的代码
训练函数
def train_model(model, train_loader, val_loader, epochs=50, lr=0.01):
# criterion = nn.MSELoss() # 推荐的损失函数
criterion = nn.BCELoss() # 推荐的损失函数
optimizer = torch.optim.Adam(model.parameters(), lr=lr)
for epoch in range(epochs):
model.train()
epoch_loss = 0.0
for inputs, targets in train_loader:
#将inputs和targets变成
inputs, targets = inputs.to(device).float(), targets.to(device).float()
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs.squeeze(), targets)
loss.backward()
optimizer.step()
epoch_loss += loss.item()
epoch_loss /= len(train_loader)
print(f"[train] Epoch {epoch + 1}/{epochs}, Loss: {epoch_loss:.7f}")
model.eval()
val_loss = 0.0
with torch.no_grad():
for inputs, targets in val_loader:
inputs, targets = inputs.to(device).float(), targets.to(device).float()
outputs = model(inputs)
loss = criterion(outputs.squeeze(), targets)
val_loss += loss.item()
val_loss /= len(val_loader)
print(f"[val] Epoch {epoch + 1}/{epochs}, Loss: {val_loss:.7f}")
print("Finished Training")
训练过程代码
train_data = xor('train')
val_data = xor('val')
train_loader = DataLoader(train_data, batch_size=256, shuffle=True)
val_loader = DataLoader(val_data, batch_size=256, shuffle=False)
model = xorModel()
model.to(device)
train_model(model, train_loader, val_loader, epochs=100, lr=0.01)
测试过程代码
# 测试模型
test_data = xor('test')
test_loader = DataLoader(test_data, batch_size=256, shuffle=False)
model.eval()
correct = 0
total = 0
with torch.no_grad():
for inputs, targets in test_loader:
inputs, targets = inputs.to(device).float(), targets.to(device).float()
outputs = model(inputs)
predicted = (outputs > 0.5).float()
correct_bits = (predicted == targets)
correct_samples = correct_bits.all(dim=1).sum().item() # 按列检查是否所有位都正确
correct += correct_samples
total += targets.size(0)
# print(f"Predicted: {predicted}, Ground Truth: {targets}")
print(f"Accuracy: {correct / total * 100:.4f} %")
最终结果
从80个epoch开始就已经完全拟合了。
最后的测试集准确率也100%
这样一个小小的实现两个16位二进制数的异或操作神经网络就完成啦