人工智能实战2——从0开始手写符号识别

接上一篇博客 人工智能实战——人工神经网络(C库iOS交叉编译),这篇博客之前发布在qq空间一直没挪过来... 

回顾下第一个例子中我用到NN(神经网络)的地方:
假设了一个目标函数 f = x==y ,设计了一个3层结构的神经网络,经过训练让神经网络自己学会如果判断两个数相等。

由于目标函数只有2个变量和一个结果,因此网络结构的输入元件只有2个,输出元件只有1个,并且输出元件的区间是[-1,1].

然而这个例子太过于简单,仅作为刚接触人工智能的一个小小的入门试探而已。接下来是时候展现人工神经的魅力所在了。把NN运用到真正适合他的场景,比如文章归类、图像识别、语音识别、用户喜好筛选等实际场景。

这篇文章就要揭开图像识别的神秘面纱,从0开始教会人工大脑如何识别手写符号,然后是手写数字,终极挑战是识别出手写汉字。在开始这篇文章前,我们先分析文字识别的几种方式:
1.能够获取用户下笔的顺序,每一笔画的方向,轻重等信息,从而将用户的笔画提取成坐标+向量集合,然后作为输入源给NN,训练NN,输出正确的结果。
2.无法获取用户下笔的顺序,轻重等信息(往往是拍照、扫描得来的图片),将图片进行简单的处理,去除噪点,黑白化,然后将各个像素点的灰度值建立成矩阵或数组,作为输入源给NN,训练NN,输出正确的结果。

方式1一般用于客户端上的手势识别,比如浏览器的手势操作等,手写输入法等,缺点是一旦无法获取下笔顺序基本上没啥用处了,通用性低。
方式2一般用于纸质文档的数字化,也就是我们常说的OCR(光学字符识别),缺点是a.需要庞大的训练数据(如果你要识别所有中国人的手写体,你需要所有中国人的手写体训练数据,包括医生开的处方^^) b.对图片的去噪、裁边、黑白过程比较难处理或者耗时间。

这里我选择了方式2,因为方式2的普适性比较高,那么首先我们的程序就需要做一点点调整:
用于识别一张图片中包含单个手写符号(假设我们来识别0~9的数字,那么一共有10中不同的输出)的程序,图片尺寸是100x100,那么这个图片就包含10000个像素点,每个像素点都有自己的颜色,输入源应该是一个100x100的矩阵,输出源要能表示10种不同的结果。

10000个像素点作为10000个输入源太庞大了,因此我们要把原始图片的质量降低一点,比如我把所有的图片压缩到16*16个像素(追求识别的精度应该扩大尺寸,牺牲性能),用来识别简单的图像。
输出的值如果是0~9的10个数字,那么输出元件如果只能输出0或1的话,我们至少需要2^4=16个排列,因此我们需要4个输出元件。

图片

这个过程看其实无非是我们第一个程序的例子中2个输入变成了16x16个,1个输出变成了4个输出,仅此而已!!而他能做的事情却发生了天翻地覆的变化。。。。这就是NN解决问题的魅力所在

只不过这次我们不能让用户从16x16个文本框中一个个来输入,而是直接让用户画,所以首先我们创建画图控件~继承UIView,用贝塞尔曲线+touchEvents可以很快写出来:

//

//  XYDrawingView.m

//  小歪

//

//  Created by reese on 16/3/23.

//  Copyright © 2016 com.ifenduo. All rights reserved.

//


#import "XYDrawingView.h"


@interface XYDrawingView ()


//所有的贝塞尔曲线

@property (nonatomicNSMutableArray *lines;


//当前的贝塞尔曲线

@property (nonatomicUIBezierPath *currentPath;


@end


@implementation XYDrawingView


- (NSMutableArray *)lines {

    if (!_lines) {

        _lines = [NSMutableArray array];

    }

    return _lines;

}


//重写drawRect方法

- (void)drawRect:(CGRect)rect {

    

    //设置描边颜色为黑色

    [[UIColor blackColorsetStroke];


    for (UIBezierPath *bezierPath in self.lines) {

        //设置线条粗细为20

        [bezierPath setLineWidth:20.0f];

        //设置转角类型为圆滑

        [bezierPath setLineJoinStyle:kCGLineJoinRound];

        //描边

        [bezierPath stroke];

    }

}


//触摸开始

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {

    CGPoint p = [[touches anyObjectlocationInView:self];

    

    //构建新的贝塞尔曲线

    _currentPath = [UIBezierPath bezierPath];

    

    //移动到第一个点

    [_currentPath moveToPoint:p];

    

    //添加到线段数组

    [self.lines addObject:_currentPath];

}


//移动

- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {

    CGPoint p = [[touches anyObjectlocationInView:self];

    

    //从当前点到上一个点连接出一条线段

    [_currentPath addLineToPoint:p];

    

    //重绘

    [self setNeedsDisplay];

}


.h文件是空的 就不列出来了。有了这个控件之后我们可以开始画符号了,在控制器中添加这个控件:
图片 
 
搞定后,我们就要讲写字区中的图像转化为数字信号,也就是刚刚说的16x16矩阵了(这里16别写死,方便我们提高精度)

转换函数:


//self.layer携带的图像信息转化成size x size容量的 float数组

- (fann_type *)getImageToBinaryForSize:(CGSize)size {

    

    //1.渲染self.layer

    

    //根据自身的frame大小开一个绘图上下文

    UIGraphicsBeginImageContext(self.bounds.size);

    //获取到这个上下文

    CGContextRef ctx = UIGraphicsGetCurrentContext();

    //渲染layer

    [self.layer renderInContext:ctx];


    //从上下文中导出image对象(压缩前的image,比如我们的绘图区大小是200x200,这个imagesize至少是200*200,如果是retina屏幕更大)

    UIImage* oImage = UIGraphicsGetImageFromCurrentImageContext();


    //关闭第一个上下文

    UIGraphicsEndImageContext();

    

    //2.压缩至size x size 大小

    

    //创建一个size x size大小的上下文

    UIGraphicsBeginImageContext(size);

    //获取到这个小号的上下文

    ctx = UIGraphicsGetCurrentContext();

    

    //把刚刚导出的image对象draw到这个小一号的上下文中,这个过程是有损压缩,压缩后还原会失真,也就是会有马赛克

    [oImage drawInRect:CGRectMake(0, 0, size.width, size.height)];

    

    //导出压缩成目标尺寸的图片,并转换成CGImageRef

    CGImageRef image = UIGraphicsGetImageFromCurrentImageContext().CGImage;

    

    //关闭第二个上下文

    UIGraphicsEndImageContext();


    //CGImageRef转换为16进制数据

    CFDataRef data = CGDataProviderCopyData(CGImageGetDataProvider(image));

    

    //获取数据长度(长度不一定等于size*size*4,size8的倍数的时候,长度等于size*size*4,其中每一个元素为216进制数,216进制数正好是一字节)

    NSInteger length = CFDataGetLength(data);

    

    //获取颜色数组

    UInt8 *colors = (UInt8 *)CFDataGetBytePtr(data);

    

    //分配一个和长度相等的目标float数组空间

    fann_type *binaryArr = malloc(sizeof(fann_type) * CFDataGetLength(data));

    

    //遍历每个像素

    for (long i = 0; i < length; i++) {

        

        //i个像素对应的r,g,b,a色值分别为colors[i*4],colors[i*4+1],colors[i*4+2],colors[i*4+3],因为这是个数组

        long index = i*4;

        

        // 对于彩色转灰度,有一个很著名的心理学公式:

        

        //Gray = R*0.299 + G*0.587 + B*0.114

        

        //这里我们简化下算法,不去计算灰度,直接用红色通道的色值来识别

        fann_type r = (CGFloat)colors[index] /255.0;

        binaryArr[i] = r;

        

    }

    return binaryArr;

}


写完之后,打一个断电观察下data的结构:
 图片

其中0xf4efefff是我在storyboard中给这个view的背景颜色,其他颜色则是我绘制出来的线条颜色的rgba值。
由于UInt8正好是一个字节,一个字节等于2个16进制数,所以每一个colors元素对应一个颜色分量。

最后我们期望得到一个恰当的16x16维数组, 那么当我们画完图之后,点击执行,输出最终得到的float数组,并按16个为一行折行,修改我们第一个例子中的执行函数:

- (IBAction)test:(id)sender {

//    fann_type input[2] = {_input1.text.floatValue,_input2.text.floatValue};

//    NSArray *output = [[XYRobotManager sharedManager] runInputDatas:input];

//    [_output setText:[output.firstObject stringValue]];

    

    //压缩的size

    int size = 16;

    

    //获取手写区图像的红色通道色值数组

    fann_type* colors = [_drawingView getImageToBinaryForSize:CGSizeMake(size, size)];

    

    //i为列,j为行

    for (int i = 0; i < size; i++) {

        for (int j = 0; j < size; j++) {

            printf("%.0f  ",colors[i*size+j]);

        }

        //每列换行

        printf("\n");

    }

}

最后测试下能否得到我们期望的数组(这里我用了%.0f,只输整数部分,第二位开始四舍五入,因此我们会看到一个由0和1组成的16x16矩阵):
 
图片

图片

图片

 除了我们最开始设想的阿拉伯数字,其实任意的黑白图像我们都能转化成float矩阵,比如三角形:
图片

接下来我们拿到这个数组后,16x16的输入源有了,那么就可以构造一个新的NN,然后训练小Y让他学会认字~过程就简单了

具体过程下篇文章再写^^

未完待续 
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
实验目的: 使用卷积神经网络(CNN)实现对MINIST手写数字0-9的识别,掌握CNN在图像识别任务中的应用。 实验步骤: 1. 数据集准备 使用MINIST手写数字数据集,该数据集包含60000个训练样本和10000个测试样本,每个样本都是28x28像素的灰度图像。可以使用PyTorch自带的torchvision.datasets.MNIST类进行数据集的加载。 2. 数据预处理 对数据集进行预处理,包括数据增强和归一化操作。数据增强可以提高模型的泛化能力,常见的数据增强方式有旋转、平移、缩放、翻转等。归一化操作可以将像素值缩放到[0,1]之间,有利于训练模型。 ```python transform_train = transforms.Compose([ transforms.RandomRotation(10), transforms.RandomAffine(0, shear=10, scale=(0.8,1.2)), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) transform_test = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) trainset = torchvision.datasets.MNIST(root='./data', train=True, download=True, transform=transform_train) trainloader = torch.utils.data.DataLoader(trainset, batch_size=64, shuffle=True, num_workers=2) testset = torchvision.datasets.MNIST(root='./data', train=False, download=True, transform=transform_test) testloader = torch.utils.data.DataLoader(testset, batch_size=64, shuffle=False, num_workers=2) ``` 3. 模型设计与训练 使用PyTorch搭建卷积神经网络模型,对手写数字图像进行分类。具体网络结构如下: ```python class Net(nn.Module): def __init__(self): super(Net, self).__init__() self.conv1 = nn.Conv2d(1, 32, 3, 1) self.conv2 = nn.Conv2d(32, 64, 3, 1) self.dropout1 = nn.Dropout2d(0.25) self.dropout2 = nn.Dropout2d(0.5) self.fc1 = nn.Linear(9216, 128) self.fc2 = nn.Linear(128, 10) def forward(self, x): x = self.conv1(x) x = F.relu(x) x = self.conv2(x) x = F.relu(x) x = F.max_pool2d(x, 2) x = self.dropout1(x) x = torch.flatten(x, 1) x = self.fc1(x) x = F.relu(x) x = self.dropout2(x) x = self.fc2(x) output = F.log_softmax(x, dim=1) return output ``` 模型训练过程: ```python device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") net = Net() net.to(device) criterion = nn.CrossEntropyLoss() optimizer = optim.SGD(net.parameters(), lr=0.01, momentum=0.9) for epoch in range(10): # loop over the dataset multiple times running_loss = 0.0 for i, data in enumerate(trainloader, 0): # get the inputs; data is a list of [inputs, labels] inputs, labels = data[0].to(device), data[1].to(device) # zero the parameter gradients optimizer.zero_grad() # forward + backward + optimize outputs = net(inputs) loss = criterion(outputs, labels) loss.backward() optimizer.step() # print statistics running_loss += loss.item() if i % 100 == 99: # print every 100 mini-batches print('[%d, %5d] loss: %.3f' % (epoch + 1, i + 1, running_loss / 100)) running_loss = 0.0 print('Finished Training') ``` 4. 模型测试 使用测试集对训练好的模型进行测试,并计算准确率。 ```python correct = 0 total = 0 with torch.no_grad(): for data in testloader: images, labels = data[0].to(device), data[1].to(device) outputs = net(images) _, predicted = torch.max(outputs.data, 1) total += labels.size(0) correct += (predicted == labels).sum().item() print('Accuracy of the network on the 10000 test images: %d %%' % ( 100 * correct / total)) ``` 实验结果: 使用上述模型,在MNIST数据集上进行训练,最终得到的准确率为98.94%。可以看出使用CNN实现手写数字识别是非常有效的。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值