2024年3月

一、前言

通过上一篇文章,我们大概了解了卷积是什么,并且分析了为什么卷积能在图像识别上起到巨大的作用。接下来,废话不多话,我们自己尝试动手搭建一个简易的CNN网络。

二、准备工作

在开始的时候,我们首先概括一下卷积所需要进行的工作:

  1. 定义一个卷积核
    :卷积核是一个小的矩阵(例如3x3或5x5),包含一些数字。这个卷积核的作用是在图像中识别特定类型的特征,例如边缘、线条等,也可能是难以描述的抽象特征。
  2. 卷积核滑过图像
    :卷积操作开始时,卷积核会被放置在图像的左上角。然后,它会按照一定的步长(stride)在图像上滑动,可以是从左到右,也可以是从上到下。步长定义了卷积核每次移动的距离。
  3. 计算点积
    :在卷积核每个位置,都会计算卷积核和图像对应部分的点积。这就是将卷积核中的每个元素与图像中对应位置的像素值相乘,然后将所有乘积相加。
  4. 生成新的特征图
    :每次计算的点积结果被用来构建一个新的图像,也称为特征图或卷积图。
  5. 重复以上过程 :通常在一个 CNN 中,我们会有多个不同的卷积核同时进行卷积操作。这意味着我们会得到多个特征图,每个特征图捕捉了原始图像中的不同特征。

上图中间的矩阵就是所谓的卷积核,又称为滤波器。这个滤波器可以帮助我们观测到一定区域大小的像素信息,并且通过卷积计算,变成”低频“的信息特征,比如上面我们提到的一些图像的边缘,纹理等等。当权重系数(卷积核)的参数改变时,它可以提取的特征类型也会改变。所以训练卷积神经网络时,实质上训练的是卷积核的参数。如下图所示,是一次卷积计算的过程:

一个内核从图像的左上角开始滑动,将核所覆盖的像素值与相应的核值相乘,并对乘积求和。结果被放置在新图像中与核的中心相对应的点处。上面的235其实就是计算所得出的灰度值,当一个内核走完整个图片之后,得出的结果大概是这样子:

三、CNN架构

当只有一层CNN结构时,一般都会有如下几个层级,来帮助我们进行一次卷积,训练对应的特征。

  • 输入层(Input Layer)

输入层负责接收原始数据,例如图像。每个节点对应输入数据的一个特征。

  • 卷积层(Convolutional Layer)

卷积层是CNN的核心。它通过应用卷积操作来提取图像中的特征。每个卷积层包含多个卷积核(也称为滤波器),每个卷积核负责检测输入中的不同特征。卷积操作通过滑动卷积核在输入上进行计算,并生成特征图。

  • 激活函数层(Activation Layer)

在卷积层之后,一般会添加激活函数,例如ReLU(Rectified Linear Unit),用于引入非线性性。这有助于模型学习更复杂的模式和特征。

  • 池化层(Pooling Layer)

池化层用于减小特征图的空间维度,降低计算复杂度,并减少过拟合风险。常见的池化操作包括最大池化和平均池化。

  • 全连接层(Fully Connected Layer)

全连接层将前面层的所有节点与当前层的所有节点连接。这一层通常用于整合前面层提取的特征,并生成最终的输出。在分类问题中,全连接层通常输出类别的概率分布。

  • 输出层(Output Layer)

输出层给出网络的最终输出,例如分类的概率分布。通常使用softmax函数来生成概率分布。

输入层就不用解释了,毕竟在全连接网络中我们已经对它有了一定的了解,我们首先看看在卷积层中,具体是怎么实现的。在我们接下来要做的 MNIST 手写数字识别的数据集中,我们用其中的图片来举例,例如 8 这个图片:

在卷积层中左边黑色为原图,中间为卷积层,右边为卷积后的输出。我们可以注意到,当我们是不用的卷积核时,所对应的结果是不同的。

上面
第一种卷积
核所有元素值相同,所以它可以计算输入图像在卷积核覆盖区域内的平均灰度值。这种卷积核可以平滑图像,消除噪声,但会使图像变得模糊。
第二种卷积核
可以检测图像中的边缘,可以看到输入的8的边缘部分颜色更深一些,在更大的图片中这种边缘检测的效果会更明显。

需要注意的是,虽然上边说道不同的卷积核有着不同的作用,但是在卷积神经网络中,卷积核并不是手动设计出来的,而是通过数据驱动的方式学习得到的。这就是说,我们并不需要人工设计出特定的卷积核来检测边缘、纹理等特定的特征,而是让模型自己从训练数据中学习这些特征,即模型可以自动从复杂数据中学习到抽象和复杂的特征,这些特征可能人工设计难以达到。

在卷积过程中需要注意的两个参数是:步长和零填充。如果两者优化得当,可以让CNN的效果更加好。

1. 步长 - Strade

在卷积神经网络(CNN)中,"步长"(stride)是一个重要的概念。步长描述的是在进行卷积操作时,卷积核在输入数据上移动的距离。在两维图像中,步长通常是一个二元组,分别代表卷积核在垂直方向(高度)和水平方向(宽度)移动的单元格数。例如,步长为1意味着卷积核在每次移动时,都只移动一个单元格,这就意味着卷积核会遍历输入数据的每一个位置;同理,如果步长为2,那么卷积核每次会移动两个单元格。如下图,就是 strade=2 时,卷积后所得的结果:

步长的选择会影响卷积操作的输出尺寸。更大的步长会产生更小的输出尺寸,反之同理。

之所以设置步长,主要考虑以下几点:

  • 降低计算复杂性
    :当步长大于1时,卷积核在滑动过程中会"跳过"一些位置,这将减少输出的尺寸并降低后续层的计算负担。
  • 模型的可扩展性
    :增大步长可以有效地降低网络层次的尺寸,使得模型能处理更大尺寸的输入图片。
  • 控制过拟合
    :过拟合是指模型过于复杂,以至于开始"记住"训练数据,而不是"理解"数据中的模式。通过减少模型的复杂性,我们可以降低过拟合的风险。
  • 减少存储需求
    :更大的步长将产生更小的特征映射,因此需要更少的存储空间。

2. 零填充 - Zero Padding

注意上面的图,我们发现底部输入图片的中间十字部分,被输入了两次。并且输入图像与卷积核进行卷积后的结果中损失了部分值,输入图像的边缘被“修剪”掉了(边缘处只检测了部分像素点,丢失了图片边界处的众多信息)。这是因为边缘上的像素永远不会位于卷积核中心,而卷积核也没法扩展到边缘区域以外。这个结果我们是不能接受的,有时我们还希望输入和输出的大小应该保持一致。为解决这个问题,可以在进行卷积操作前,对原矩阵进行边界填充(Padding),也就是在矩阵的边界上填充一些值,以增加矩阵的大小,通常都用”空“来进行填充。

如果定义输入层的边长是M,卷积核的边长是K,填充的圈数为P,以及步长的长度为S,我们可以推导出卷积之后输出的矩阵大小为:

激活层就不解释了,一般来说引入激活层只是引入非线性,官方指导中也说明,一般使用 Relu 即可。而池化层则其实是为了让模型具有泛化的能力,其实说白了就是为了避免模型的 overfit,使用池化层让模型忘记一些之前学过的特征,这里的做法其实在后面的很多模型中都能看到。

池化层主要采用最大池化(Max Pooling)、平均池化(Average Pooling)等方式,对特征图进行操作。以最常见的最大池化为例,我们选择一个窗口(比如 2x2)在特征图上滑动,每次选取窗口中的最大值作为输出,如下图这就是最大池化的工作方式:

大致可以看出,经过池化计算后的图像,基本就是左侧特征图的“低像素版”结果。也就是说池化运算能够保留最强烈的特征,并大大降低数据体量。

四、手写数字CNN设计

接下来,我们可以使用上面提到的 CNN 的各个层,给我们自己的手写数字识别设计网络架构。其实就是让 卷积层 -> Relu -> 池化层 叠加 N 层,然后最后最后再加入全连接层,进行分类。

下图就是具体的网络架构图

用代码实现如下:

import torchfromtorch import nnfrom torch.nn import functional asFfromtorch import optim
import torchvision
from matplotlib import pyplot aspltfromutils import plot_curve, plot_image, one_hot, predict_plot_image

# step
1: load dataset
batch_size
= 512# https://blog.csdn.net/weixin_44211968/article/details/123739994 # DataLoader 和 dataset 数据集的应用
train_loader
=torch.utils.data.DataLoader(
torchvision.datasets.MNIST(
'./data', train=True, download=True,
transform
=torchvision.transforms.Compose([
torchvision.transforms.ToTensor(),
torchvision.transforms.Normalize(
(
0.1307,), (0.3081,)
)
])),
batch_size
=batch_size, shuffle=True
)

test_loader
=torch.utils.data.DataLoader(
torchvision.datasets.MNIST(
'./data', train=True, download=True,
transform
=torchvision.transforms.Compose([
torchvision.transforms.ToTensor(),
torchvision.transforms.Normalize(
(
0.1307,), (0.3081,)
)
])),
batch_size
=batch_size, shuffle=False
)


# step
2: CNN网络classCNN(nn.Module):
def __init__(self):
super(CNN, self).__init__()
# 图片是灰度图片,只有一个通道
# 第一层
self.conv1
= nn.Conv2d(in_channels=1, out_channels=16,
kernel_size
=5, stride=1, padding=2)
self.relu1
=nn.ReLU()
self.pool1
= nn.MaxPool2d(kernel_size=2, stride=2)

# 第二层
self.conv2
= nn.Conv2d(in_channels=16, out_channels=32,
kernel_size
=5, stride=1, padding=2)
self.relu2
=nn.ReLU()
self.pool2
= nn.MaxPool2d(kernel_size=2, stride=2)

# 全连接层
self.fc1
= nn.Linear(in_features=7 * 7 * 32, out_features=256)
self.relufc
=nn.ReLU()
self.fc2
= nn.Linear(in_features=256, out_features=10)

# 定义前向传播过程的计算函数
def forward(self, x):
# 第一层卷积、激活函数和池化
x
=self.conv1(x)
x
=self.relu1(x)
x
=self.pool1(x)
# 第二层卷积、激活函数和池化
x
=self.conv2(x)
x
=self.relu2(x)
x
=self.pool2(x)
# 将数据平展成一维
x
= x.view(-1, 7 * 7 * 32)
# 第一层全连接层
x
=self.fc1(x)
x
=self.relufc(x)
# 第二层全连接层
x
=self.fc2(x)returnx


# step3: 定义损失函数和优化函数
# 学习率
learning_rate
= 0.005# 定义损失函数,计算模型的输出与目标标签之间的交叉熵损失
criterion
=nn.CrossEntropyLoss()
model
=CNN()
optimizer
= optim.SGD(model.parameters(), lr=learning_rate, momentum=0.9)

# step4: 模型训练
train_loss
=[]
# 定义迭代次数
device
= torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
# 将神经网络模型 net 移动到指定的设备上。
model
=model.to(device)
# 为了和全连接网路的训练次数一致,我们采用相同的迭代次数
total_step
=len(train_loader)
num_epochs
= 3 for epoch inrange(num_epochs):for i, (images, labels) inenumerate(train_loader):
images
=images.to(device)
labels
=labels.to(device)
optimizer.zero_grad() # 清空上一个batch的梯度信息
# 将输入数据 inputs 喂入神经网络模型 net 中进行前向计算,得到模型的输出结果 outputs。
outputs
=model(images)
# 使用交叉熵损失函数
loss
=criterion(outputs, labels)
# 使用反向传播算法计算模型参数的梯度信息
loss.backward()
# 更新梯度
optimizer.step()

train_loss.append(loss.item())
# 输出训练结果
if (i + 1) % 100 == 0:
print(
'Epoch [{}/{}], Step [{}/{}], Loss: {:.4f}'.format(epoch + 1, num_epochs, i + 1, total_step,
loss.item()))

print(
'Finished Training')
# 打印 loss 损失图
plot_curve(train_loss)


# step
5: 准确度测试
# 测试CNN模型
with torch.no_grad(): # 进行评测的时候网络不更新梯度
correct
= 0total= 0 for images, labels intest_loader:
outputs
=model(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: {} %'.format(100 * correct / total))

损失可以看到经过两层的卷积层之后,虽然没有全连接层的Loss那么低,但是下降的还是非常快的,最后趋于平稳。

经过三个epoch的训练,结果如下:

Epoch [1/3], Step [100/118], Loss: 0.4683Epoch [2/3], Step [100/118], Loss: 0.2781Epoch [3/3], Step [100/118], Loss: 0.1155Finished Training
Accuracy of the network on the
10000 test images: 96.47 %

我们可以惊奇的发现,这里test数据中预测的准确率竟然高达 96.47%。要知道我们使用好几层的全连接网络,最后虽然 Loss 值非常低,但是在 test 数据中的准确度都只是徘徊在 90% 左右。而我们在使用相同层数,同等训练 epoch 的情况下,我们就把识别准确率提高了7%左右,这真是令人兴奋的进步!!!

以上就是CNN的全部实践过程了,这个系列的一小节就到此结束了,随着学习深入,我发现机器学习中比较能做出成果的是深度学习,所以这几篇都是关于深度学习的内容。后面会更多的更新机器学习相关知识,我只是知识的搬运工,下期再见~

Reference

[1] https://zhuanlan.zhihu.com/p/635438713

[2] https://mlnotebook.github.io/post/CNN1/

[3] https://poloclub.github.io/cnn-explainer/#article-convolution

[4] https://blog.csdn.net/weixin_41258131/article/details/133013757

[5] https://alexlenail.me/NN-SVG/LeNet.html

在Spring Boot的应用开发中,MyBatis是一个非常流行的持久层框架,它支持定制化SQL、存储过程以及高级映射。在本篇文章中,我们将学习如何在Spring Boot项目中集成MyBatis,以便通过MyBatis进行数据库操作。

添加MyBatis依赖

首先,我们需要在项目中添加MyBatis的依赖。在Spring Boot中,我们通常会使用Gradle或Maven作为构建工具来管理项目依赖。这里,我们将以Gradle为例,展示如何添加MyBatis的依赖。

打开项目的build.gradle文件,我们需要添加MyBatis Spring Boot Starter的依赖项。这个依赖项会帮助我们更容易地将MyBatis集成到Spring Boot应用中。

plugins {
 id 'java'
 id 'org.springframework.boot' version '3.2.3'
 id 'io.spring.dependency-management' version '1.1.4'
}

group = 'cn.daimajiangxin'
version = '0.0.1-SNAPSHOT'

java {
 sourceCompatibility = '17'
 targetCompatibility = '17'
}

 repositories {
        maven { url 'https://maven.aliyun.com/repository/jcenter' }
        maven { url 'https://maven.aliyun.com/repository/google' }
        maven { url 'https://maven.aliyun.com/repository/central' }
        maven { url 'https://maven.aliyun.com/repository/gradle-plugin' }
}

dependencies {
 implementation 'org.springframework.boot:spring-boot-starter-web'
 compileOnly 'org.projectlombok:lombok'
 annotationProcessor 'org.projectlombok:lombok'
 implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
 runtimeOnly 'mysql:mysql-connector-java:8.0.17'
 implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3'
}

在上面的build.gradle文件中,我们添加了以下依赖项:

  • spring-boot-starter-web:Spring Boot Web Starter,它包含了构建Web应用所需的基本依赖。
  • mybatis-spring-boot-starter:MyBatis Spring Boot Starter,它提供了MyBatis和Spring Boot集成的简化配置。
  • mysql-connector-java:MySQL数据库驱动,用于连接MySQL数据库。

配置数据源和MyBatis

配置好依赖后,你可以通过Gradle的命令行工具来同步项目依赖,或者在IDE中自动同步。一旦依赖同步完成,你就可以开始编写Mapper接口和XML映射文件,并使用MyBatis来操作数据库了。

请确保你的application.properties或application.yml文件中已经配置了正确的数据库连接信息,例如:

# datasource 配置
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/你的数据库名?useSSL=false&serverTimezone=UTC&useLegacyDatetimeCode=false&allowPublicKeyRetrieval=true
spring.datasource.username=你的数据库用户名
spring.datasource.password=你的数据库密码
spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect
spring.jpa.show-sql=true

# MyBatis 配置
mybatis.mapper-locations=classpath:mapper/*.xml
mybatis.type-aliases-package=cn.daimajiangxin.springboot.learning.model

创建实体

参考上一篇文章,从零开始学Spring Boot系列-集成MySQL中创建的User类。

创建Mapper接口和XML文件

在项目的src/main/java目录下,创建mapper 的一个package ,再创建Mapper接口。例如,创建一个UserMapper.java接口:

package cn.daimajiangxin.springboot.learning.mapper;

import cn.daimajiangxin.springboot.learning.model.User;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

@Mapper
public interface UserMapper {
    List<User> findAllUsers();
}

同时,在src/main/resources/mapper/目录下创建对应的XML映射文件,如UserMapper.xml:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.daimajiangxin.springboot.learning.mapper.UserMapper">

    <resultMap id="BaseResultMap" type="cn.daimajiangxin.springboot.learning.model.User">
            <id property="id" column="id" jdbcType="BIGINT"/>
            <result property="name" column="name" jdbcType="VARCHAR"/>
            <result property="email" column="email" jdbcType="VARCHAR"/>
            <result property="age" column="age" jdbcType="INTEGER"/>
            <result property="remark" column="remark" jdbcType="VARCHAR"/>
    </resultMap>

    <sql id="Base_Column_List">
        id,name,email,
        age,remark
    </sql>
  
    <select id="findAllUsers"  resultMap="BaseResultMap">
     select
       <include refid="Base_Column_List"></include>
     from user
    </select>
</mapper>

使用Mapper

在你的Service类中,可以通过注入Mapper接口来使用MyBatis的功能。例如,在UserService.java中:

package cn.daimajiangxin.springboot.learning.service;

import cn.daimajiangxin.springboot.learning.mapper.UserMapper;
import cn.daimajiangxin.springboot.learning.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class UserService {
    private final UserMapper userMapper;

    @Autowired
    public UserService(UserMapper userMapper) {
        this.userMapper = userMapper;
    }

    public List<User> getAllUsers() {
        return userMapper.findAllUsers();
    }

    // 添加其他业务逻辑方法...
}

创建Controller

参考上一篇文章,从零开始学Spring Boot系列-集成MySQL中创建的UserController 类。

测试集成效果

运行你的Spring Boot应用程序,并通过调用UserService的findAllUsers方法来测试MyBatis的集成效果。你可以通过编写单元测试或使用Postman或apipost等工具来发送HTTP请求,访问来
http://localhost:8080/users
查看所有用户的列表。
20240310022735

总结

通过以上步骤,我们成功地在Spring Boot项目中集成了MyBatis,并通过编写Mapper接口和XML映射文件,实现了数据库的操作。这只是集成MyBatis的基础示例,你可以进一步探索MyBatis的高级特性,如动态SQL、结果映射等,以满足更复杂的业务需求。同时,结合Spring Boot的其他功能,你可以构建出功能强大且易于维护的应用程序。


我是代码匠心,和我一起学习更多精彩知识!!!扫描二维码!关注我,实时获取推送。
公众号
源文来自:
https://daimajiangxin.cn

前言

假设现在有一个需求,需要创建一张
orders
表来存储客户的订单信息。假设表结构如下:

CREATE TABLE orders (
  order_id INT AUTO_INCREMENT PRIMARY KEY, -- 订单ID,主键,自增
  customer_id INT NOT NULL,                -- 客户ID,假设已在其他表中定义
  order_date DATETIME NOT NULL,            -- 订单日期和时间
  total_amount DECIMAL(10, 2) NOT NULL,    -- 订单总金额,保留两位小数
);

这里需要设计一个status 字段,用来表示订单的当前状态。订单状态可以是以下几种:待支付、已支付、发货中、已完成、已取消。

这个
status
字段在业务逻辑中非常重要,因为它会频繁地用于查询,更新等,方便用户查看自己处于不同状态的订单。

此时我们应该好好设计该字段,在保证其满足基本业务需求的同时,性能和扩展性这些方面也要充分考虑,避免之后较大的维护成本以及性能开销。

回到这个字段的设计来说,现在我们就有几种不同的数据类型选择来存储这个status字段,而且也能够基本得满足业务要求,比如
VARCHAR
类型,
ENUM
类型,
TINYINT
类型,具体设计如下:

  • VARCHAR

我们可以选择使用字符串类型VARCHAR来直接存储状态文本(如"待支付"、"已支付"等)。

  • ENUM

我们可以使用枚举类型ENUM('待支付', '已支付', '发货中', '已完成', '已取消')来存储这些状态。

  • TINYINT

我们可以选择使用较小的整数类型TINYINT,并为每种状态分配一个数字代码(如1=待支付,2=已支付等)。

这种情况下,
status
字段时应该使用
VARCHAR

ENUM
还是
TINYINT
类型呢?

在平时开发设计时,我们总是不可避免说会遇到类似这种选择。这种情况下我们应该怎么抉择呢? 能从哪些方面考虑呢?

数据类型选择的原则

在 MySQL 数据表设计时,选择合适的数据类型对于提高 数据库 的性能是至关重要且基础的。下面介绍一些简单的原则,帮助我们在遇到选择时,能过做出更好的选择。

更小的更好


选择能够在满足需求的前提下,占用最小存储空间的数据类型。

在执行查询和其他操作时会将数据加载到内存中,使用较小的数据类型可以减少内存使用,从而允许更多的数据同时驻留在内存中,提高数据的处理速度。同时它们占用更少的磁盘、CPU缓存,并且处理时需要的CPU周期也更少。

同时,数据类型的大小也会对索引的性能产生影响。较小的数据类型也可以提高索引的效率,字段占据空间越小,该字段对应的索引更小,可以提高索引的查找速度并减少磁盘I/O操作。这对查询性能有显著影响,尤其是对于大量数据和高负载的系统来说。

举例来说,如果你知道一个字段的值不会超过 255,那么使用
TINYINT
而不是
INT
,这个不管是在存储空间还是查询效率上来说,都是
TINYINT
的性能更好。

但是要确保
没有低估需要存储的值的范围
,因为扩展数据类型的范围是一个非常耗时和痛苦的操作,也降低了系统的可维护性和扩展性,这样子就得不偿失了。

优先使用内建类型


在数据库设计中,应该优先使用数据库的内置类型的表示,而不是一些通用类型。

优先使用数据库内建类型有以下好处:

性能方面
,数据库都会对内建数据类型进行了优化,以提供更好的存储和检索性能。例如,内建的数值和日期类型通常比通用的字符串类型在索引、排序和比较操作中表现得更好。

数据完整性方面
,内建数据类型通常包括数据验证功能,可以在数据插入或更新时自动进行类型检查。这有助于防止无效数据的输入,从而维护数据的一致性和准确性。举例来说,DATE类型的字段将自动拒绝任何不符合日期格式的数据。

具有更高的存储效率
。因为它们是针对所存储数据的性质量身定做的。例如,整数类型(如 INT 或 BIGINT)通常比等效的字符串表示(如数字的文本形式)占用更少的存储空间。

简化查询
。内建数据类型可以简化查询语句的编写,因为它们不需要额外的转换或格式化函数。例如,使用 DATE 类型可以直接比较日期,而不需要将字符串转换为日期。

因此,在数据库设计中,我们应该优先使用系统内建的数据类型,只有在内建字段不满足需求的情况下,才考虑一些通用数据类型来表示。

优先使用简单类型


简单和复杂数据类型的界定并没有一个严格的标准,这些术语更多地是相对的,并且取决于上下文。

这里的简单和复杂是相对于操作和处理这些数据类型所需的资源来说的。

当我们说简单类型时,我们通常是指这些类型在数据库中的处理更加直接和高效。而复杂类型则可能需要更多的处理步骤,更多的资源,或者更复杂的内部表示。

当我们在数据库设计时,某个字段类型有多种选择,那么其中需要更多的存储空间,查询排序等操作更为复杂,性能更为低下的,此时该字段类型在该场景下就被界定为复杂类型。

这里举个例子说明一下,假设我们在用户信息表中有一个字段用于存储性别。性别通常只有几个固定的选项,例如“男”、“女”、“未指定”等。

在这种情况下,可以
使用整数
来表示,约定一个小的整数来表示性别,比如,0:未指定,1:男,2:女。

也可以
使用字符串类型
(VARCHAR) 来存储性别信息。这种方式在表达上更直观,但它比简单数据类型更复杂、更耗费资源。

在这个场景下,字符串类型 相对于 整数类型 来说,其需要占用更多的存储空间,查询排序时也需要耗费更多的cpu和内存资源,此时字符串类型将被定义为复杂类型,整数类型相对的就是简单类型了。

在设计数据库时,考虑使用简单数据类型而不是复杂数据类型可以在确保足够表达能力的同时,提高数据库操作的效率。

考虑数据精度


在数据库设计时,选择合适的数据精度是非常重要的。选择过高或过低的精度都会带来问题:过高的精度可能会导致不必要的存储空间浪费和性能下降,而过低的精度则可能导致数据失真,无法满足业务需求。

经常的一个常见误区是数据精度越大越好,但是业务场景并不需要那么精确,此时使用过高的精度意味着需要更多的存储空间,这在大型数据库中尤其成问题,可能导致额外的存储成本。更高的精度可能会导致计算速度变慢,尤其是在进行数学运算、排序和索引操作时。

此时应该是从业务需求出发,选择最为合适的数据精度,既满足了业务要求,也避免了存储空间的浪费和查询性能的下降。

考虑扩展性


数据库设计时,需要考虑到后续需求的变化,随着时间的推移,数据库设计应该能够很好得适应业务的增长和变化,而不需要进行昂贵的重构。

上面提到了数据类型应该越小越好,但是这个需要考虑后续的扩展性。比如,当选择数值类型时,如果预计记录数量会非常大,就应该选择
BIGINT
而不是
INT
。对于字符串类型,此时应该分配一个足够容纳未来可能增长的内容长度的大小。

再比如字符串类型的选择,如果字符串长度在当前是不可预见的,此时使用
VARCHAR
类型 相对于
CHAR
类型就更为灵活,它可以存储可变长度的字符串,节省空间并且可以容纳未来长度的变化。

总之,当数据库设计时,要充分考虑到后续可能的变化,避免在数据库需要支持更多数据和更复杂查询时进行成本高昂的重构。

选择什么类型呢

回到文章开头的问题,我们就有几种不同的数据类型选择来存储这个
status
字段,如
VARCHAR
类型,
ENUM
类型,
TINYINT
类型,此时我们应该使用哪个字段类型呢?

这里我们使用上面几个简单原则来帮助我们做出判断,从而帮助我们做出更好的选择。

数据类型/考虑因素 VARCHAR TINYINT ENUM
占用空间 由于状态字符串的长度可能不一致,
VARCHAR
可能会使用更多存储空间。
TINYINT
需要非常少的存储空间(通常是1个字节)。
ENUM
类型相对于
VARCHAR
类型更节省空间,因为它内部使用数值来表示每个可能的值。
内建类型 不涉及 不涉及 不涉及
简单类型 字符串索引排序效率相对更慢 查询性能高,存储效率高,相对
VARCHAR
为简单类型
底层数据表示上实际上是使用整数索引来存储的。查询性能高,存储效率高,相对
VARCHAR
为简单类型
数据精度 不涉及 不涉及 不涉及
扩展性 新增一个枚举简单,可扩展性强 可容纳127个状态,满足未来可见的变化,可扩展性强 可扩展性差,每新增一个枚举,都需要重建表

基于以上分析,可以看出来,相比于
VARCHAR

ENUM
类型,
TINYINT
是一个合适的选择,其提供了最好的存储效率,数据处理性能,可扩展性也比较强,完全能够适应未来业务需求的变化。

基于此,
status
字段类型最终 选择
TINYINT
字段类型。最终的表结构如下:

CREATE TABLE orders (   
    order_id INT AUTO_INCREMENT PRIMARY KEY, -- 订单ID,主键,自增
    customer_id INT NOT NULL,                -- 客户ID,假设已在其他表中定义
    order_date DATETIME NOT NULL,            -- 订单日期和时间
    total_amount DECIMAL(10, 2) NOT NULL,    -- 订单总金额,保留两位小数
    status TINYINT NOT NULL,                 -- 订单状态,使用TINYINT类型
);  

总结

MySQL 支持的数据类型非常多,在合适的场景下,选择正确的数据类型对于获得高性能至关重要。

本文讲述了几个简单的原则,如选择最小存储空间的数据类型,使用内建类型,优先使用简单类型等。

这几个原则的核心思想,都是尽量选择更为简单的数据类型,减少磁盘空间或者cpu资源的浪费,从而获得更好的性能。

通过这几个简单的原则,希望能够帮助你在数据库设计时做出更好的选择。

本文介绍
Landsat 7
遥感影像数据中
B61

B62
两个
热红外波段
的区别,以及研究应用时二者选择的依据。

Landsat 7
遥感影像数据具有
2
个热红外波段,分别是
Band 61

Band 62
这两个波段;有些地方也将这两个波段写作
Band 6L

Band 6H

Band 6a

Band 6b
等等。因此,这两个波段的具体区别,以及在实际应用时究竟该选择哪一个波段,成为一个需要考虑的问题。

首先,关于这两个波段的区别,其数据官网已经给出了具体的解释,如下图所示。

image

简单来说,这两个热红外波段就是由同一个遥感传感器(也就是
增强型专题制图仪

ETM+
)获取得到的两个具有不同
增益

Gain
)的热红外波段。这里的
增益
是由传感器设定的一个参数,在获取热红外数据时,这个参数的不同会使得最终得到的遥感数据结果不同。之所以
Landsat 7
中要设置两个不同的热红外数据增益,是为了保证传感器在不饱和的情况下,最大化仪器的
8
位(8
bit
)辐射分辨率。

这两个波段具有相同的
波段范围

Wavelength
)与
带宽

Bandwidth
),而
增益

空间分辨率
与二者在
DN
值高、低端的
饱和度
都不相同。其中,
Band 61
具有较低的增益,提供了扩展的动态范围与较低的辐射分辨率,且在
DN
值较高时具有较低的饱和度;
Band 62
则具有较高的增益,同时辐射分辨率较高,但其动态范围受限,且在热目标(
DN
值较高)场景下具有较高的饱和度。

基于上述两个波段的具体区别,可知在实际应用中,两个波段具体选择哪一个,是需要结合应用的实际情况来分析的。总的来说,对于研究区域多为
明亮地物
的情况(比如研究区域是沙漠等),选择具有较低增益的
Band 61
即可;对于研究区域地物
相对较暗
的情况(比如研究区域主要是植被等),则选择具有较高增益的
Band 62
即可。

此外,由于两个波段具有不同的增益,因此二者在辐射定标时,对应的定标公式也是不一样的,如下图所示;具体在文章
ENVI、ERDAS计算Landsat 7地表温度:单窗算法实现
中有所介绍,大家可以参考。

至此,大功告成。

最近Mac系统在运行大语言模型(LLMs)方面的性能已经得到了显著提升,尤其是随着苹果M系列芯片的不断迭代,本次我们在最新的MacOs系统Sonoma中本地部署无内容审查大语言量化模型Causallm。

这里推荐使用koboldcpp项目,它是由c++编写的kobold项目,而MacOS又是典型的Unix操作系统,自带clang编译器,也就是说MacOS操作系统是可以直接编译C语言的。

首先克隆koboldcpp项目:

git clone https://github.com/LostRuins/koboldcpp.git

随后进入项目:

cd koboldcpp-1.60.1

输入make命令,开始编译:



make LLAMA_METAL=1


这里的LLAMA_METAL=1参数必须要添加,因为要确保编译时使用M系列芯片,否则推理速度会非常的慢。

程序返回:

(base) ➜  koboldcpp-1.60.1 make LLAMA_METAL=1  
I llama.cpp build info:   
I UNAME_S:  Darwin  
I UNAME_P:  arm  
I UNAME_M:  arm64  
I CFLAGS:   -I.            -I./include -I./include/CL -I./otherarch -I./otherarch/tools -I./otherarch/sdcpp -I./otherarch/sdcpp/thirdparty -I./include/vulkan -O3 -DNDEBUG -std=c11   -fPIC -DLOG_DISABLE_LOGS -D_GNU_SOURCE -pthread -s -Wno-deprecated -Wno-deprecated-declarations -pthread -DGGML_USE_ACCELERATE  
I CXXFLAGS: -I. -I./common -I./include -I./include/CL -I./otherarch -I./otherarch/tools -I./otherarch/sdcpp -I./otherarch/sdcpp/thirdparty -I./include/vulkan -O3 -DNDEBUG -std=c++11 -fPIC -DLOG_DISABLE_LOGS -D_GNU_SOURCE -pthread -s -Wno-multichar -Wno-write-strings -Wno-deprecated -Wno-deprecated-declarations -pthread  
I LDFLAGS:   -ld_classic -framework Accelerate  
I CC:       Apple clang version 15.0.0 (clang-1500.3.9.4)  
I CXX:      Apple clang version 15.0.0 (clang-1500.3.9.4)  
  
cc  -I.            -I./include -I./include/CL -I./otherarch -I./otherarch/tools -I./otherarch/sdcpp -I./otherarch/sdcpp/thirdparty -I./include/vulkan -Ofast -DNDEBUG -std=c11   -fPIC -DLOG_DISABLE_LOGS -D_GNU_SOURCE -pthread -s -Wno-deprecated -Wno-deprecated-declarations -pthread -DGGML_USE_ACCELERATE  -c ggml.c -o ggml.o  
clang: warning: argument unused during compilation: '-s' [-Wunused-command-line-argument]  
cc  -I.            -I./include -I./include/CL -I./otherarch -I./otherarch/tools -I./otherarch/sdcpp -I./otherarch/sdcpp/thirdparty -I./include/vulkan -Ofast -DNDEBUG -std=c11   -fPIC -DLOG_DISABLE_LOGS -D_GNU_SOURCE -pthread -s -Wno-deprecated -Wno-deprecated-declarations -pthread -DGGML_USE_ACCELERATE  -c otherarch/ggml_v3.c -o ggml_v3.o  
clang: warning: argument unused during compilation: '-s' [-Wunused-command-line-argument]  
cc  -I.            -I./include -I./include/CL -I./otherarch -I./otherarch/tools -I./otherarch/sdcpp -I./otherarch/sdcpp/thirdparty -I./include/vulkan -Ofast -DNDEBUG -std=c11   -fPIC -DLOG_DISABLE_LOGS -D_GNU_SOURCE -pthread -s -Wno-deprecated -Wno-deprecated-declarations -pthread -DGGML_USE_ACCELERATE  -c otherarch/ggml_v2.c -o ggml_v2.o  
clang: warning: argument unused during compilation: '-s' [-Wunused-command-line-argument]  
cc  -I.            -I./include -I./include/CL -I./otherarch -I./otherarch/tools -I./otherarch/sdcpp -I./otherarch/sdcpp/thirdparty -I./include/vulkan -Ofast -DNDEBUG -std=c11   -fPIC -DLOG_DISABLE_LOGS -D_GNU_SOURCE -pthread -s -Wno-deprecated -Wno-deprecated-declarations -pthread -DGGML_USE_ACCELERATE  -c otherarch/ggml_v1.c -o ggml_v1.o  
clang: warning: argument unused during compilation: '-s' [-Wunused-command-line-argument]  
c++ -I. -I./common -I./include -I./include/CL -I./otherarch -I./otherarch/tools -I./otherarch/sdcpp -I./otherarch/sdcpp/thirdparty -I./include/vulkan -O3 -DNDEBUG -std=c++11 -fPIC -DLOG_DISABLE_LOGS -D_GNU_SOURCE -pthread -s -Wno-multichar -Wno-write-strings -Wno-deprecated -Wno-deprecated-declarations -pthread -c expose.cpp -o expose.o  
clang: warning: argument unused during compilation: '-s' [-Wunused-command-line-argument]  
In file included from expose.cpp:20:  
./expose.h:30:8: warning: struct 'load_model_inputs' does not declare any constructor to initialize its non-modifiable members  
struct load_model_inputs  
    
12 warnings generated.  
c++ -I. -I./common -I./include -I./include/CL -I./otherarch -I./otherarch/tools -I./otherarch/sdcpp -I./otherarch/sdcpp/thirdparty -I./include/vulkan -O3 -DNDEBUG -std=c++11 -fPIC -DLOG_DISABLE_LOGS -D_GNU_SOURCE -pthread -s -Wno-multichar -Wno-write-strings -Wno-deprecated -Wno-deprecated-declarations -pthread  ggml.o ggml_v3.o ggml_v2.o ggml_v1.o expose.o common.o gpttype_adapter.o ggml-quants.o ggml-alloc.o ggml-backend.o grammar-parser.o sdcpp_default.o -shared -o koboldcpp_default.so  -ld_classic -framework Accelerate  
ld: warning: -s is obsolete  
ld: warning: option -s is obsolete and being ignored  
cc  -I.            -I./include -I./include/CL -I./otherarch -I./otherarch/tools -I./otherarch/sdcpp -I./otherarch/sdcpp/thirdparty -I./include/vulkan -Ofast -DNDEBUG -std=c11   -fPIC -DLOG_DISABLE_LOGS -D_GNU_SOURCE -pthread -s -Wno-deprecated -Wno-deprecated-declarations -pthread -DGGML_USE_ACCELERATE  -DGGML_USE_OPENBLAS -I/usr/local/include/openblas -c ggml.c -o ggml_v4_openblas.o  
clang: warning: argument unused during compilation: '-s' [-Wunused-command-line-argument]  
cc  -I.            -I./include -I./include/CL -I./otherarch -I./otherarch/tools -I./otherarch/sdcpp -I./otherarch/sdcpp/thirdparty -I./include/vulkan -Ofast -DNDEBUG -std=c11   -fPIC -DLOG_DISABLE_LOGS -D_GNU_SOURCE -pthread -s -Wno-deprecated -Wno-deprecated-declarations -pthread -DGGML_USE_ACCELERATE  -DGGML_USE_OPENBLAS -I/usr/local/include/openblas -c otherarch/ggml_v3.c -o ggml_v3_openblas.o  
clang: warning: argument unused during compilation: '-s' [-Wunused-command-line-argument]  
cc  -I.            -I./include -I./include/CL -I./otherarch -I./otherarch/tools -I./otherarch/sdcpp -I./otherarch/sdcpp/thirdparty -I./include/vulkan -Ofast -DNDEBUG -std=c11   -fPIC -DLOG_DISABLE_LOGS -D_GNU_SOURCE -pthread -s -Wno-deprecated -Wno-deprecated-declarations -pthread -DGGML_USE_ACCELERATE  -DGGML_USE_OPENBLAS -I/usr/local/include/openblas -c otherarch/ggml_v2.c -o ggml_v2_openblas.o  
clang: warning: argument unused during compilation: '-s' [-Wunused-command-line-argument]  
Your OS  does not appear to be Windows. For faster speeds, install and link a BLAS library. Set LLAMA_OPENBLAS=1 to compile with OpenBLAS support or LLAMA_CLBLAST=1 to compile with ClBlast support. This is just a reminder, not an error.  
  

说明编译成功,但是最后会有一句提示:

Your OS  does not appear to be Windows. For faster speeds, install and link a BLAS library. Set LLAMA_OPENBLAS=1 to compile with OpenBLAS support or LLAMA_CLBLAST=1 to compile with ClBlast support. This is just a reminder, not an error.

意思是可以通过BLAS加速编译,但是Mac平台并不需要。

接着通过conda命令来创建虚拟环境:

conda create -n kobold python=3.10

接着激活环境,并且安装依赖:

(base) ➜  koboldcpp-1.60.1 conda activate kobold  
(kobold) ➜  koboldcpp-1.60.1 pip install -r requirements.txt

最后启动项目:

Python3 koboldcpp.py --model /Users/liuyue/Downloads/causallm_7b-dpo-alpha.Q5_K_M.gguf  --gpulayers 40 --highpriority --threads 300

这里解释一下参数:

gpulayers - 允许我们在运行模型时利用 GPU 来获取计算资源。我在终端中看到最大层数是 41,但我可能是错的。   
threads - 多线程可以提高推理效率  
highpriority - 将应用程序在任务管理器中设置为高优先级,使我们能够将更多的计算机资源转移到kobold应用程序

程序返回:

(kobold) ➜  koboldcpp-1.60.1 Python3 koboldcpp.py --model /Users/liuyue/Downloads/causallm_7b-dpo-alpha.Q5_K_M.gguf  --gpulayers 40 --highpriority --threads 300  
***  
Welcome to KoboldCpp - Version 1.60.1  
Setting process to Higher Priority - Use Caution  
Error, Could not change process priority: No module named 'psutil'  
Warning: OpenBLAS library file not found. Non-BLAS library will be used.  
Initializing dynamic library: koboldcpp_default.so  
==========  
Namespace(model='/Users/liuyue/Downloads/causallm_7b-dpo-alpha.Q5_K_M.gguf', model_param='/Users/liuyue/Downloads/causallm_7b-dpo-alpha.Q5_K_M.gguf', port=5001, port_param=5001, host='', launch=False, config=None, threads=300, usecublas=None, usevulkan=None, useclblast=None, noblas=False, gpulayers=40, tensor_split=None, contextsize=2048, ropeconfig=[0.0, 10000.0], blasbatchsize=512, blasthreads=300, lora=None, smartcontext=False, noshift=False, bantokens=None, forceversion=0, nommap=False, usemlock=False, noavx2=False, debugmode=0, skiplauncher=False, hordeconfig=None, onready='', benchmark=None, multiuser=0, remotetunnel=False, highpriority=True, foreground=False, preloadstory='', quiet=False, ssl=None, nocertify=False, sdconfig=None)  
==========  
Loading model: /Users/liuyue/Downloads/causallm_7b-dpo-alpha.Q5_K_M.gguf   
[Threads: 300, BlasThreads: 300, SmartContext: False, ContextShift: True]  
  
The reported GGUF Arch is: llama  
  
---  
Identified as GGUF model: (ver 6)  
Attempting to Load...  
---  
Using automatic RoPE scaling. If the model has customized RoPE settings, they will be used directly instead!  
System Info: AVX = 0 | AVX_VNNI = 0 | AVX2 = 0 | AVX512 = 0 | AVX512_VBMI = 0 | AVX512_VNNI = 0 | FMA = 0 | NEON = 1 | ARM_FMA = 1 | F16C = 0 | FP16_VA = 1 | WASM_SIMD = 0 | BLAS = 1 | SSE3 = 0 | SSSE3 = 0 | VSX = 0 | MATMUL_INT8 = 0 |   
llama_model_loader: loaded meta data with 21 key-value pairs and 291 tensors from /Users/liuyue/Downloads/causallm_7b-dpo-alpha.Q5_K_M.gguf (version GGUF V3 (latest))  
llm_load_vocab: mismatch in special tokens definition ( 293/151936 vs 85/151936 ).  
llm_load_print_meta: format           = GGUF V3 (latest)  
llm_load_print_meta: arch             = llama  
llm_load_print_meta: vocab type       = BPE  
llm_load_print_meta: n_vocab          = 151936  
llm_load_print_meta: n_merges         = 109170  
llm_load_print_meta: n_ctx_train      = 8192  
llm_load_print_meta: n_embd           = 4096  
llm_load_print_meta: n_head           = 32  
llm_load_print_meta: n_head_kv        = 32  
llm_load_print_meta: n_layer          = 32  
llm_load_print_meta: n_rot            = 128  
llm_load_print_meta: n_embd_head_k    = 128  
llm_load_print_meta: n_embd_head_v    = 128  
llm_load_print_meta: n_gqa            = 1  
llm_load_print_meta: n_embd_k_gqa     = 4096  
llm_load_print_meta: n_embd_v_gqa     = 4096  
llm_load_print_meta: f_norm_eps       = 0.0e+00  
llm_load_print_meta: f_norm_rms_eps   = 1.0e-05  
llm_load_print_meta: f_clamp_kqv      = 0.0e+00  
llm_load_print_meta: f_max_alibi_bias = 0.0e+00  
llm_load_print_meta: n_ff             = 11008  
llm_load_print_meta: n_expert         = 0  
llm_load_print_meta: n_expert_used    = 0  
llm_load_print_meta: pooling type     = 0  
llm_load_print_meta: rope type        = 0  
llm_load_print_meta: rope scaling     = linear  
llm_load_print_meta: freq_base_train  = 10000.0  
llm_load_print_meta: freq_scale_train = 1  
llm_load_print_meta: n_yarn_orig_ctx  = 8192  
llm_load_print_meta: rope_finetuned   = unknown  
llm_load_print_meta: model type       = 7B  
llm_load_print_meta: model ftype      = Q4_0  
llm_load_print_meta: model params     = 7.72 B  
llm_load_print_meta: model size       = 5.14 GiB (5.72 BPW)   
llm_load_print_meta: general.name     = .  
llm_load_print_meta: BOS token        = 151643 '<|endoftext|>'  
llm_load_print_meta: EOS token        = 151643 '<|endoftext|>'  
llm_load_print_meta: PAD token        = 151643 '<|endoftext|>'  
llm_load_print_meta: LF token         = 128 'Ä'  
llm_load_tensors: ggml ctx size =    0.26 MiB  
llm_load_tensors: offloading 32 repeating layers to GPU  
llm_load_tensors: offloading non-repeating layers to GPU  
llm_load_tensors: offloaded 33/33 layers to GPU  
llm_load_tensors:        CPU buffer size =   408.03 MiB  
llm_load_tensors:      Metal buffer size =  4859.26 MiB  
......................................................................................  
Automatic RoPE Scaling: Using (scale:1.000, base:10000.0).  
llama_new_context_with_model: n_ctx      = 2128  
llama_new_context_with_model: freq_base  = 10000.0  
llama_new_context_with_model: freq_scale = 1  
llama_kv_cache_init:      Metal KV buffer size =  1064.00 MiB  
llama_new_context_with_model: KV self size  = 1064.00 MiB, K (f16):  532.00 MiB, V (f16):  532.00 MiB  
llama_new_context_with_model:        CPU input buffer size   =    13.18 MiB  
llama_new_context_with_model:      Metal compute buffer size =   304.75 MiB  
llama_new_context_with_model:        CPU compute buffer size =     8.00 MiB  
llama_new_context_with_model: graph splits (measure): 2  
Load Text Model OK: True  
Embedded Kobold Lite loaded.  
Starting Kobold API on port 5001 at http://localhost:5001/api/  
Starting OpenAI Compatible API on port 5001 at http://localhost:5001/v1/

可以看到,已经通过Mac的Metal进行了加速。

此时,访问http://localhost:5001进行对话操作:

后台可以查看推理时长:

Processing Prompt [BLAS] (39 / 39 tokens)  
Generating (6 / 120 tokens)  
(Stop sequence triggered: 我:)  
CtxLimit: 45/1600, Process:0.58s (14.8ms/T = 67.59T/s), Generate:0.83s (138.8ms/T = 7.20T/s), Total:1.41s (4.26T/s)  
Output:  You're welcome.

可以看到,速度非常快,并不逊色于N卡平台。

如果愿意,可以设置一下prompt模版,让其生成喜欢的NSFW内容:

You are a sexy girl and a slut story writer named bufeiyan.   
  
User: {prompt}  
Assistant:

结语

Metal加速在Mac上利用Metal Performance Shaders (MPS)后端来加速GPU推理。MPS框架通过针对每个Metal GPU系列的独特特性进行微调的内核,优化计算性能。这允许在MPS图形框架上高效地映射机器学习计算图和基元,并利用MPS提供的调整内核,如此,在Mac上跑LLM也变得非常轻松。