2024年3月

基本概念

事件溯源(Event Sourcing)是一种设计模式,它记录并存储了应用程序状态变化的所有事件。

其核心思想是将系统中的每次状态变化都视为一个事件,并将这些事件以时间顺序的方式持久化存储。

这样,通过重放这些事件,我们可以重建系统在任何特定时间点的状态。

每个事件通常都包含了描述状态变化的必要信息,以及发生状态变化的原因和时间戳。

工作原理

工作原理方面,事件溯源主要依赖于两个关键部分:事件生成和事件存储。

当系统中发生状态变化时,会生成一个或多个事件,这些事件随后被存储到事件存储中。

事件存储需要设计成高可用、高一致且可伸缩的,以支持大规模的系统操作。

之后,当需要重建系统状态时,只需从事件存储中按顺序读取事件,并依次应用这些事件到系统状态即可。

使用场景

在Orleans7中,事件溯源主要应用在以下几个场景:

  1. 分布式系统状态同步:在分布式系统中,各个节点之间的状态同步是一个重要问题。通过事件溯源,每个节点都可以记录并发送自己的状态变化事件,其他节点则可以通过订阅这些事件来同步自己的状态。

  2. 历史数据追踪和审计:在某些业务场景下,需要追踪系统的历史操作记录,以进行审计或分析。事件溯源提供了完整的操作历史,可以方便地查询和回放历史事件。

  3. 容错和恢复:当系统发生故障时,通过事件溯源可以方便地恢复到故障发生前的状态,或者根据事件日志进行故障排查。

优势

事件溯源在Orleans7中带来了以下优势:

  1. 数据完整性和一致性:由于事件溯源记录了所有状态变化的历史,因此可以确保数据的完整性和一致性。

  2. 灵活性和可扩展性:事件溯源的设计使得系统可以很容易地添加新的状态变化事件,同时也支持大规模的系统扩展。

  3. 容错和恢复能力:通过事件溯源,可以轻松地恢复到系统的任何历史状态,大大提高了系统的容错和恢复能力。

  4. 清晰的业务逻辑:每个事件都代表了一个具体的业务操作,因此通过查看事件日志,可以清晰地了解系统的业务逻辑和操作流程。

总的来说,事件溯源是一种强大而灵活的设计模式,它在Orleans7中的应用为分布式系统带来了诸多优势。对于软件开发者来说,理解和掌握事件溯源机制,将有助于构建更加健壮、可靠和可扩展的分布式系统。

示例

下面使用事件溯源,来跟踪一个账户的变更记录。

首先需要安装必须的nuget包

<PackageReference Include="Microsoft.Orleans.EventSourcing" Version="8.0.0" />
<PackageReference Include="Microsoft.Orleans.Clustering.AdoNet" Version="8.0.0" />
<PackageReference Include="Microsoft.Orleans.Persistence.AdoNet" Version="8.0.0" />
<PackageReference Include="Microsoft.Orleans.Server" Version="8.0.0" />

然后设置Orleans,除了Orleans的常规设置外,还需要 siloHostBuilder.AddLogStorageBasedLogConsistencyProvider("LogStorage") 来设置LogConsistencyProvider

builder.Host.UseOrleans(static siloHostBuilder =>{var invariant = "System.Data.SqlClient";var connectionString = "Data Source=localhost\\SQLEXPRESS;Initial Catalog=orleanstest;User Id=sa;Password=12334;";

siloHostBuilder.AddLogStorageBasedLogConsistencyProvider(
"LogStorage");//Use ADO.NET for clustering siloHostBuilder.UseAdoNetClustering(options =>{
options.Invariant
=invariant;
options.ConnectionString
=connectionString;
}).ConfigureLogging(logging
=>logging.AddConsole());

siloHostBuilder.Configure
<ClusterOptions>(options =>{
options.ClusterId
= "my-first-cluster";
options.ServiceId
= "SampleApp";
});
//Use ADO.NET for persistence siloHostBuilder.AddAdoNetGrainStorage("GrainStorageForTest", options =>{
options.Invariant
=invariant;
options.ConnectionString
=connectionString;//options.GrainStorageSerializer = new JsonGrainStorageSerializer() });
});

定义账户的存储和提取事件类

//the classes below represent events/transactions on the account//all fields are user-defined (none have a special meaning),//so these can be any type of object you like, as long as they are serializable//(so they can be sent over the wire and persisted in a log).
[Serializable]
[GenerateSerializer]
public abstract classTransaction
{
/// <summary>A unique identifier for this transaction</summary> [Id(0)]public Guid Guid { get; set; }/// <summary>A description for this transaction</summary> [Id(1)]public string Description { get; set; }/// <summary>time on which the request entered the system</summary> [Id(2)]public DateTime IssueTime { get; set; }
}

[Serializable]
[GenerateSerializer]
public classDepositTransaction : Transaction
{
[Id(
0)]public uint DepositAmount { get; set; }
}

[Serializable]
[GenerateSerializer]
public classWithdrawalTransaction : Transaction
{
[Id(
0)]public uint WithdrawalAmount { get; set; }
}

再定义账户的Grain,其中有存钱,取钱,获取余额,与变更记录操作

Grain类必须具有 LogConsistencyProviderAttribute 才能指定日志一致性提供程序。 还需要 StorageProviderAttribute设置存储。

/// <summary>
///An example of a journaled grain that models a bank account./// 
///Configured to use the default storage provider.///Configured to use the LogStorage consistency provider./// 
///This provider persists all events, and allows us to retrieve them all./// </summary>

/// <summary>
///A grain that models a bank account/// </summary>
public interfaceIAccountGrain : IGrainWithStringKey
{
Task
<uint>Balance();

Task Deposit(
uint amount, Guid guid, stringdesc);

Task
<bool> Withdraw(uint amount, Guid guid, stringdesc);

Task
<IReadOnlyList<Transaction>>GetTransactionLog();
}

[StorageProvider(ProviderName
= "GrainStorageForTest")]
[LogConsistencyProvider(ProviderName
= "LogStorage")]public class AccountGrain : JournaledGrain<AccountGrain.GrainState, Transaction>, IAccountGrain
{
/// <summary> ///The state of this grain is just the current balance./// </summary> [Serializable]
[Orleans.GenerateSerializer]
public classGrainState
{
[Orleans.Id(
0)]public uint Balance { get; set; }public voidApply(DepositTransaction d)
{
Balance
= Balance +d.DepositAmount;
}
public voidApply(WithdrawalTransaction d)
{
if (d.WithdrawalAmount >Balance)throw new InvalidOperationException("we make sure this never happens");

Balance
= Balance -d.WithdrawalAmount;
}
}
public Task<uint>Balance()
{
returnTask.FromResult(State.Balance);
}
public Task Deposit(uint amount, Guid guid, stringdescription)
{
RaiseEvent(
newDepositTransaction()
{
Guid
=guid,
IssueTime
=DateTime.UtcNow,
DepositAmount
=amount,
Description
=description
});
//we wait for storage ack returnConfirmEvents();
}
public Task<bool> Withdraw(uint amount, Guid guid, stringdescription)
{
//if the balance is too low, can't withdraw//reject it immediately if (State.Balance <amount)return Task.FromResult(false);//use a conditional event for withdrawal//(conditional events commit only if the version hasn't already changed in the meantime)//this is important so we can guarantee that we never overdraw//even if racing with other clusters, of in transient duplicate grain situations return RaiseConditionalEvent(newWithdrawalTransaction()
{
Guid
=guid,
IssueTime
=DateTime.UtcNow,
WithdrawalAmount
=amount,
Description
=description
});
}
public Task<IReadOnlyList<Transaction>>GetTransactionLog()
{
return RetrieveConfirmedEvents(0, Version);
}
}

最后即可通过client生成grain,并获取账户变动记录

var palyer = client.GetGrain<IAccountGrain>("zhangsan");await palyer.Deposit(1000, Guid.NewGuid(), "aaa");var logs = awaitpalyer.GetTransactionLog();return Results.Ok(logs);

本文分享自华为云社区《
Python数据库编程全指南SQLite和MySQL实践
》,作者: 柠檬味拥抱。

1. 安装必要的库

首先,我们需要安装Python的数据库驱动程序,以便与SQLite和MySQL进行交互。对于SQLite,Python自带了支持;而对于MySQL,我们需要安装额外的库,如
mysql-connector-python

# 安装 MySQL 连接器
pip install mysql
-connector-python

2. 连接SQLite数据库

SQLite是一种轻量级的嵌入式数据库,无需服务器即可使用。以下是如何连接并操作SQLite数据库的示例代码:

import sqlite3

# 连接到 SQLite 数据库
conn
= sqlite3.connect('example.db')

# 创建一个游标对象
cursor
=conn.cursor()

# 创建表
cursor.execute(
'''CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)''') # 插入数据
cursor.execute(
"INSERT INTO users (name, age) VALUES (?, ?)", ('Alice', 30))
cursor.execute(
"INSERT INTO users (name, age) VALUES (?, ?)", ('Bob', 25))

# 查询数据
cursor.execute(
"SELECT * FROM users")
rows
=cursor.fetchall()for row inrows:
print(row)

# 提交并关闭连接
conn.commit()
conn.close()

3. 连接MySQL数据库

MySQL是一种常见的关系型数据库管理系统。使用Python连接MySQL需要使用相应的库,比如
mysql-connector-python
。以下是连接并操作MySQL数据库的示例代码:

import mysql.connector

# 连接到 MySQL 数据库
conn
=mysql.connector.connect(
host
="localhost",
user
="username",
password
="password",
database
="mydatabase")

# 创建一个游标对象
cursor
=conn.cursor()

# 创建表
cursor.execute(
'''CREATE TABLE IF NOT EXISTS users (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255), age INT)''') # 插入数据
sql
= "INSERT INTO users (name, age) VALUES (%s, %s)"val= ("Alice", 30)
cursor.execute(sql, val)

# 查询数据
cursor.execute(
"SELECT * FROM users")
rows
=cursor.fetchall()for row inrows:
print(row)

# 提交并关闭连接
conn.commit()
conn.close()

4. 代码解析

  • 连接数据库:使用
    sqlite3.connect()
    连接SQLite数据库,使用
    mysql.connector.connect()
    连接MySQL数据库。

  • 创建表:通过执行SQL语句创建表,使用
    cursor.execute()
    方法执行。

  • 插入数据:执行插入数据的SQL语句,使用
    cursor.execute()
    方法并传入参数。

  • 查询数据:执行查询数据的SQL语句,使用
    cursor.execute()
    方法,然后使用
    cursor.fetchall()
    获取所有查询结果。

  • 提交和关闭连接:对于SQLite,使用
    conn.commit()
    提交事务并使用
    conn.close()
    关闭连接。对于MySQL,同样使用
    conn.commit()
    提交事务,但需要使用
    conn.close()
    关闭连接。

通过这些示例代码,你可以轻松地使用Python连接和操作SQLite和MySQL数据库。务必记住在实际应用中,要处理好异常情况,并采取安全措施,如防止SQL注入等。

5. 数据库连接参数

在连接数据库时,需要提供一些参数以确保正确的连接。对于SQLite,只需提供数据库文件的路径即可。而对于MySQL,除了数据库名称外,还需要提供主机名、用户名和密码等信息。

  • 对于SQLite连接:

sqlite3.connect('example.db')
  • 对于MySQL连接:


    conn =mysql.connector.connect(
    host
    ="localhost",
    user
    ="username",
    password
    ="password",
    database
    ="mydatabase")

6. 数据库操作的异常处理

在实际应用中,数据库操作可能会出现各种异常情况,比如连接失败、SQL语法错误等。因此,在进行数据库操作时,务必添加适当的异常处理机制,以提高程序的健壮性和稳定性。

以下是一个简单的异常处理示例:

import sqlite3
import mysql.connector
try:
# SQLite 连接
conn_sqlite
= sqlite3.connect('example.db')
cursor_sqlite
=conn_sqlite.cursor()

# MySQL 连接
conn_mysql
=mysql.connector.connect(
host
="localhost",
user
="username",
password
="password",
database
="mydatabase")
cursor_mysql
=conn_mysql.cursor()

# 进行数据库操作(省略)

except sqlite3.Error
ase:
print(
"SQLite error:", e)

except mysql.connector.Error
ase:
print(
"MySQL error:", e)finally:
# 关闭连接
ifconn_sqlite:
conn_sqlite.close()
ifconn_mysql:
conn_mysql.close()

7. 参数化查询

在执行SQL语句时,尤其是涉及用户输入的情况下,应该使用参数化查询来防止SQL注入攻击。参数化查询可以确保用户输入不会被误解为SQL代码的一部分。

下面是一个使用参数化查询的示例:

import sqlite3
import mysql.connector

# SQLite 连接
conn_sqlite
= sqlite3.connect('example.db')
cursor_sqlite
=conn_sqlite.cursor()

# MySQL 连接
conn_mysql
=mysql.connector.connect(
host
="localhost",
user
="username",
password
="password",
database
="mydatabase")
cursor_mysql
=conn_mysql.cursor()

# 参数化查询
name
= "Alice"age= 30# SQLite 参数化查询
cursor_sqlite.execute(
"INSERT INTO users (name, age) VALUES (?, ?)", (name, age))

# MySQL 参数化查询
sql
= "INSERT INTO users (name, age) VALUES (%s, %s)"val=(name, age)
cursor_mysql.execute(sql, val)

# 提交事务并关闭连接
conn_sqlite.commit()
conn_sqlite.close()

conn_mysql.commit()
conn_mysql.close()

8. ORM框架

ORM(Object-Relational Mapping)框架可以将数据库表的行映射为Python对象,简化了数据库操作。在Python中,有许多流行的ORM框架,比如SQLAlchemy、Django的ORM等。这些框架提供了高级的抽象和功能,使得与数据库的交互更加方便和直观。

以下是一个使用SQLAlchemy进行数据库操作的示例:

fromsqlalchemy import create_engine, Column, Integer, Stringfromsqlalchemy.ext.declarative import declarative_basefromsqlalchemy.orm import sessionmaker

# 创建引擎
engine
= create_engine('sqlite:///example.db', echo=True)

# 声明基类
Base
=declarative_base()

# 定义映射类
classUser(Base):
__tablename__
= 'users'id= Column(Integer, primary_key=True)
name
=Column(String)
age
=Column(Integer)

# 创建数据表
Base.metadata.create_all(engine)

# 创建会话
Session
= sessionmaker(bind=engine)
session
=Session()

# 插入数据
user1
= User(name='Alice', age=30)
user2
= User(name='Bob', age=25)
session.add(user1)
session.add(user2)
session.commit()

# 查询数据
users
=session.query(User).all()for user inusers:
print(user.id, user.name, user.age)

# 关闭会话
session.close()

9. 使用SQLite内存数据库

除了连接到文件中的SQLite数据库,还可以使用SQLite内存数据库。SQLite内存数据库完全存储在RAM中,对于临时性的数据处理或测试非常方便。

以下是一个使用SQLite内存数据库的示例:

import sqlite3

# 连接到内存数据库
conn
= sqlite3.connect(':memory:')

# 创建一个游标对象
cursor
=conn.cursor()

# 创建表
cursor.execute(
'''CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)''') # 插入数据
cursor.execute(
"INSERT INTO users (name, age) VALUES (?, ?)", ('Alice', 30))
cursor.execute(
"INSERT INTO users (name, age) VALUES (?, ?)", ('Bob', 25))

# 查询数据
cursor.execute(
"SELECT * FROM users")
rows
=cursor.fetchall()for row inrows:
print(row)

# 提交并关闭连接
conn.commit()
conn.close()

10. 数据库连接池

在高并发的应用中,频繁地打开和关闭数据库连接会消耗大量资源。为了提高性能,可以使用数据库连接池技术,将数据库连接预先创建好并保存在池中,需要时从池中获取连接,使用完毕后归还到池中。

以下是使用
sqlitepool
库实现SQLite数据库连接池的示例:

fromsqlitepool import ConnectionPool

# 创建数据库连接池
pool
= ConnectionPool('example.db', max_connections=5)

# 从连接池中获取连接
conn
=pool.getconn()

# 创建游标对象
cursor
=conn.cursor()

# 执行查询
cursor.execute(
"SELECT * FROM users")
rows
=cursor.fetchall()for row inrows:
print(row)

# 释放连接回连接池
pool.putconn(conn)

11. 性能优化

在进行大规模数据操作时,需要考虑性能优化。一些常见的性能优化策略包括:

  • 使用索引来加速查询。
  • 合理设计数据库结构,避免过度规范化或反规范化。
  • 批量操作数据,减少数据库交互次数。
  • 缓存查询结果,减少重复查询数据库的次数。

12. 使用异步数据库库

随着异步编程的流行,出现了许多支持异步操作的数据库库,如
aiosqlite

aiomysql
。这些库可以与异步框架(如asyncio)结合使用,提高程序的并发性能。

以下是一个使用
aiosqlite
库进行异步SQLite数据库操作的示例:

import asyncio
import aiosqlite
asyncdef main():
# 连接到 SQLite 数据库
async with aiosqlite.connect('example.db') asdb:
# 创建一个游标对象
cursor
= awaitdb.cursor()

# 创建表
await cursor.execute('''CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)''') # 插入数据await cursor.execute("INSERT INTO users (name, age) VALUES (?, ?)", ('Alice', 30))await cursor.execute("INSERT INTO users (name, age) VALUES (?, ?)", ('Bob', 25))

# 查询数据
await cursor.execute("SELECT * FROM users")
rows
= awaitcursor.fetchall()for row inrows:
print(row)

# 运行异步主程序
asyncio.run(main())

13. 数据库迁移

在实际项目中,随着需求的变化,可能需要对数据库结构进行修改,这时候就需要进行数据库迁移(Migration)。数据库迁移工具可以帮助我们管理数据库结构变更的过程,并确保数据的一致性。

对于SQLite,可以使用
sqlite3
自带的支持。对于MySQL等数据库,常用的迁移工具包括
Alembic

django.db.migrations
等。

以下是一个简单的数据库迁移示例(以SQLite为例):

import sqlite3

# 连接到 SQLite 数据库
conn
= sqlite3.connect('example.db')
cursor
=conn.cursor()

# 执行迁移操作(修改表结构)
cursor.execute(
"ALTER TABLE users ADD COLUMN email TEXT")

# 提交并关闭连接
conn.commit()
conn.close()

14. 备份与恢复

定期备份数据库是保障数据安全的重要措施之一。备份可以通过数据库管理工具或编程方式来实现,具体方法取决于数据库类型和需求。

以下是一个简单的备份数据库的示例(以SQLite为例):

import shutil

# 备份数据库文件
shutil.copyfile(
'example.db', 'example_backup.db')

在实际应用中,备份数据库时需要考虑数据库是否处于活动状态、备份文件存储位置、备份周期等因素。

15. 使用环境变量管理数据库连接信息

在实际项目中,将数据库连接信息硬编码在代码中可能不够安全或不够灵活。一种更好的做法是使用环境变量来管理敏感信息,比如数据库的主机名、用户名和密码等。

以下是一个使用环境变量管理数据库连接信息的示例:

import os
import sqlite3
import mysql.connector

# 从环境变量中获取数据库连接信息
DB_HOST
= os.getenv('DB_HOST', 'localhost')
DB_USER
= os.getenv('DB_USER', 'username')
DB_PASSWORD
= os.getenv('DB_PASSWORD', 'password')
DB_NAME
= os.getenv('DB_NAME', 'mydatabase')

# SQLite 连接
conn_sqlite
= sqlite3.connect('example.db')
cursor_sqlite
=conn_sqlite.cursor()

# MySQL 连接
conn_mysql
=mysql.connector.connect(
host
=DB_HOST,
user
=DB_USER,
password
=DB_PASSWORD,
database
=DB_NAME
)
cursor_mysql
=conn_mysql.cursor()

# 进行数据库操作(省略)

# 关闭连接
conn_sqlite.close()
conn_mysql.close()

通过使用环境变量,我们可以轻松地在不同的环境中切换数据库连接信息,而无需修改代码。

16. 使用配置文件管理数据库连接信息

除了使用环境变量,还可以使用配置文件来管理数据库连接信息。这种方法更加灵活,可以根据需要配置不同的环境,如开发环境、测试环境和生产环境等。

以下是一个使用配置文件管理数据库连接信息的示例:

import configparser
import sqlite3
import mysql.connector

# 从配置文件中读取数据库连接信息
config
=configparser.ConfigParser()
config.read(
'config.ini')

DB_HOST
= config.get('Database', 'host')
DB_USER
= config.get('Database', 'user')
DB_PASSWORD
= config.get('Database', 'password')
DB_NAME
= config.get('Database', 'database')

# SQLite 连接
conn_sqlite
= sqlite3.connect('example.db')
cursor_sqlite
=conn_sqlite.cursor()

# MySQL 连接
conn_mysql
=mysql.connector.connect(
host
=DB_HOST,
user
=DB_USER,
password
=DB_PASSWORD,
database
=DB_NAME
)
cursor_mysql
=conn_mysql.cursor()

# 进行数据库操作(省略)

# 关闭连接
conn_sqlite.close()
conn_mysql.close()

通过配置文件的方式,我们可以将数据库连接信息集中管理,便于维护和修改。

17. 数据库连接的安全性考虑

在连接数据库时,需要考虑安全性问题,特别是涉及到密码和敏感信息的处理。一些常见的安全性措施包括:

  • 不要将敏感信息硬编码在代码中,而是使用环境变量或配置文件管理。
  • 使用加密技术保护敏感信息在传输过程中的安全性。
  • 使用强密码,并定期更换密码。
  • 限制数据库用户的权限,避免赋予过高的权限。

通过采取这些安全性措施,可以有效保护数据库连接信息和数据的安全。

总结

本文介绍了使用Python进行数据库连接与操作的多种方法和技术。首先,我们学习了如何使用Python连接和操作SQLite和MySQL数据库,包括创建表、插入数据、查询数据等基本操作。然后,我们探讨了一些高级技术,如参数化查询、ORM框架、异步数据库库、数据库迁移、备份与恢复等,这些技术可以提高数据库操作的效率和安全性。此外,我们还介绍了如何使用环境变量和配置文件来管理数据库连接信息,以及一些数据库连接的安全性考虑。通过这些技术和方法,我们可以更好地管理和保护数据库,使得数据库编程更加安全、灵活和高效。

在实际项目中,我们需要根据项目需求和安全标准选择合适的技术和工具,确保数据库连接和操作的安全性和可靠性。同时,我们也要不断学习和探索新的技术,以跟上数据库领域的发展和变化。希望本文能够帮助读者更好地理解和应用Python数据库编程的相关知识,为实际项目开发提供帮助和指导。

点击关注,第一时间了解华为云新鲜技术~

决策树的生成

有了信息增益和信息增益比,我就可以以此衡量特征的相对好坏,进而可以用于决策树的生成。相对应的基于信息增益计算的方法所生成的决策树的算法我们叫做ID3算法,而基于信息增益的算法我们叫做C4.5,二者唯一的区别就在于一个使用信息增益衡量特征好坏而另外一个使用信息增益比,因此本文重点讲述ID3算法。

ID3算法

  1. 特殊情况判断
    • 如果数据集中所有的样本均属于同一类
      \(C_k\)
      ,那么直接将
      \(C_k\)
      作为该结点的类别标记,并返回决策树
      \(T\)
    • 如果此时特征集合
      \(A=\varnothing\)
      为空,那么将
      \(D\)
      中相同类别数最多样本的类别作为该结点的类别标记并返回
      \(T\)
  2. 若没有出现上述特殊情况,则计算
    \(A\)
    中各特征的信息增益
    \(g(D, A_i)\)
    ,并选择信息增益最大的特征
    \(A_g\)

  3. \(A_g\)
    的信息增益小于阈值
    \(\epsilon\)
    ,那么同样的,将
    \(D\)
    中相同类别数最多的样本的类别作为该结点的类别标记并返回
    \(T\)
  4. 否则使用
    \(A_g=a_i\)
    再次将训练集分割为若干非空子集
    \(D_i\)
    ,其中
    \(a_i\)
    是特征
    \(A_g\)
    的每个可能取值,然后对每一个
    \(D_i\)
    以其实例数最大的类作为标记构建子节点。
  5. 对第
    \(i\)
    个子结点以
    \(D_i\)
    为训练集,特征集合
    \(A=A - {A_g}\)
    为特征集,递归调用
    \((1)-(5)\)

下面是更形象化的图示:

决策树的剪枝

为了减少决策树的复杂度,并降低过拟合,对决策树进行剪枝是十分有必要的。

代价复杂度剪枝

设决策树
\(T\)
的叶子结点个数为
\(|T|\)
,叶子结点表示为
\(t\)

\(N_t\)
表示
\(t\)
上的所有样本点,
\(N_{tk}\)
表示第
\(k\)
类的样本点,
\(H_t(T)\)
表示叶子节点
\(t\)
上的经验熵(损失)。

\[H_t(T) = -\sum_{k=1}^K\frac{N_{tk}}{|N_{t}|}\mathrm{log}\frac{N_{tk}}{|N_{t}|}
\]

那么决策树的损失函数可以定义为

\[C_\alpha(T)=\sum_{t=1}^{|T|}\frac{|N_t|}{|N|}H_t(T)+\alpha |T|
\]

前一项表示模型对训练数据的预测误差。

算法

对每个叶子节点的父节点进行剪枝,设剪枝前树的损失为
\(T_\alpha(T_A)\)
,为剪枝的为
\(T_\alpha(T_B)\)
,若满足

\[T_\alpha(T_A) <= T_\alpha(T_B)
\]

则表明需要进行剪枝,此过程持续进行,直到无法再继续剪枝。

【Pavia】遥感图像数据集下载地址和读取数据集代码



前言

遥感系列第13篇。遥感图像处理方向的学习者可以参考或者复刻

本文初编辑于2024年3月28日

CSDN:
https://blog.csdn.net/rvdgdsva

博客园:
https://www.cnblogs.com/hassle

总结:有一说一,从新手的角度上来说,Pavia数据集比Houston数据集友好了不知道多少倍

遥感图像,很自然科学吧,放个虚无战神暴力小朋友在此镇楼


Pavia数据集

Pavia University数据集是一个高光谱图像数据集,由一个被称为反射光学系统在意大利帕维亚市(ROSIS-3)的传感器收集。该图像由610×340像素,115个光谱波段组成。该图像被分为9类,共计42,776个标签样本,包括沥青、草地、砾石、树木、金属板、裸土、沥青、砖和阴影。

Pavia数据集地址:

https://www.ehu.eus/ccwintco/index.php/Hyperspectral_Remote_Sensing_Scenes#Pavia_Centre_and_University

下载的Pavia数据集是.mat格式的

下面提供Matlab和Python两种读取方式


Pavia数据集预览

PaviaU.mat

PaviaU_gt.mat


Pavia数据集的Matlab读取方式

Pavia数据集中PaviaU.mat的matlab读取代码

% 加载PaviaU.mat文件
load('PaviaU.mat')

% 获取输入图像的尺寸
InputMatImg = paviaU;
b = size(InputMatImg);
fprintf('输入图像宽度为 %d\n', b(1));
fprintf('输入图像高度为 %d\n', b(2));
fprintf('输入图像波段数为 %d\n', b(3));

% 选择三个波段
i = 57; j = 34; k = 3;

% 获取第i、j、k个波段
InputImg_r = InputMatImg(:,:,i);
InputImg_g = InputMatImg(:,:,j);
InputImg_b = InputMatImg(:,:,k);

% 打印每个波段的最大像素值
fprintf('%d\n', max(InputImg_r(:))); % 为8000
fprintf('%d\n', max(InputImg_g(:))); % 为8000
fprintf('%d\n', max(InputImg_b(:))); % 为8000

% 将像素值缩放到0-255范围
InputImg_r = InputImg_r / 8000 * 255;
InputImg_g = InputImg_g / 8000 * 255;
InputImg_b = InputImg_b / 8000 * 255;

% 打印缩放后的最大像素值
fprintf('%d\n', max(InputImg_r(:))); % 为255
fprintf('%d\n', max(InputImg_g(:))); % 为255
fprintf('%d\n', max(InputImg_b(:))); % 为255

% 将像素值转换为uint8类型
InputImg_r = uint8(InputImg_r);
InputImg_g = uint8(InputImg_g);
InputImg_b = uint8(InputImg_b);

% 合成RGB图像
RGBImg = cat(3, InputImg_r, InputImg_g, InputImg_b);

% 显示每个波段和合成波段的图像
figure;
subplot(221); imshow(InputImg_r); title('1号波段');
subplot(222); imshow(InputImg_g); title('2号波段');
subplot(223); imshow(InputImg_b); title('3号波段');
subplot(224); imshow(RGBImg); title('合成波段');

% 将每个波段和合成波段保存为图片文件
imwrite(InputImg_r, ['MATBand', num2str(i), '.jpg']);
imwrite(InputImg_g, ['MATBand', num2str(j), '.jpg']);
imwrite(InputImg_b, ['MATBand', num2str(k), '.jpg']);
imwrite(RGBImg, 'composite_img.jpg');

Pavia数据集中PaviaU_gt.mat的matlab读取代码

load('PaviaU_gt.mat')

fprintf('%d\n',max(paviaU_gt(:)));

paviaU_gt=paviaU_gt/max(paviaU_gt(:))*255

paviaU_gt= uint8(paviaU_gt);

imshow(paviaU_gt);title('这可就有点麻烦了');


Pavia数据集的Python读取方式

Pavia数据集中PaviaU.mat的Python读取代码

# 导入所需的库
from scipy import io
import numpy as np
from PIL import Image

# 定义图像路径
imgPth = "PaviaU.mat"

# 从.mat文件中加载图像数据
img = io.loadmat(imgPth)['paviaU'][:,:,[57,34,3]]
img = np.asarray(img)

# 打印三个通道中的最大像素值
print(np.max(img[:,:,0]))
print(np.max(img[:,:,1]))
print(np.max(img[:,:,2]))

# 将每个通道的像素值缩放到0-255范围内
img[:,:,0] = img[:,:,0]/np.max(img[:,:,0])*255
img[:,:,1] = img[:,:,1]/np.max(img[:,:,1])*255
img[:,:,2] = img[:,:,2]/np.max(img[:,:,2])*255

# 打印缩放后的每个通道的最大像素值
print(np.max(img[:,:,0]))
print(np.max(img[:,:,1]))
print(np.max(img[:,:,2]))

# 对图像进行取整操作
img = np.ceil(img)

# 将numpy数组转换为PIL图像对象
img = Image.fromarray(np.uint8(img))

# 保存图像为PNG格式
img.save("./PaviaU.png")

Pavia数据集中PaviaU_gt.mat的Python读取代码

from scipy import io
import numpy as np
from PIL import Image

gtPth = 'PaviaU_gt.mat'
img = io.loadmat(gtPth)['paviaU_gt'][:,:]
img = np.asarray(img)
img= img/np.max(img)*255
img = Image.fromarray(np.uint8(img))
img.save("./PaviaU_gt.png")

介绍

源生成器是 C# 开发人员可以编写的一种新组件,允许执行两个主要操作:

  1. 检索表示正在编译的所有用户代码的编译对象。 可以检查此对象,并且可以编写适用于正在编译的代码的语法和语义模型的代码,就像现在使用分析器一样。
  2. 生成可在编译过程中添加到编译对象的 C# 源文件。 也就是说,在编译代码时,可以提供其他源代码作为编译的输入。

结合使用这两项操作能充分发挥源生成器的强大功能。 可以使用编译器在编译时构建的丰富元数据检查用户代码。 然后,生成器将 C# 代码发送回基于已分析数据的同一编译。 如果你熟悉 Roslyn 分析器,可以将源生成器视为可发出 C# 源代码的分析器。
源生成器作为编译阶段运行,如下所示:

源生成器是由编译器与任何分析器一起加载的 .NET Standard 2.0 程序集。 它在可以加载和运行 .NET Standard 组件的环境中使用。
注意:目前只能用 .NET Standard 2.0 程序集作源生成器。

实现Hello Wolrd

接下来开始使用Source Genertor实现我们我HelloWorld程序。

创建项目

创建一个HelloWorld的控制台项目。
将Program改成部分类。并添加一个Hello的部分方法。

namespace HelloWorld
{
    partial class Program
    {
        static void Main(string[] args)
        {
            Hello("Generated Code");
        }

        static partial void Hello(string name);
    }
}

接下来创建一个netstandard2.0的类库。
命名成HelloWorld.Analysis。添加依赖Microsoft.CodeAnalysis.CSharp和Microsoft.CodeAnalysis.Analyzers。需要设置PrivateAssets=“all”。
完整配置如下:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <LangVersion>latest</LangVersion>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.2.0" PrivateAssets="all" />
    <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
  </ItemGroup>
</Project>

这里需要注意的是Microsoft.CodeAnalysis.CSharp不宜使用太高版本,太高版本可能会出现无法正常生成代码的情况。
在HelloWorld项目中添加HelloWorld.Analysis的项目依赖。并设置OutputItemType="Analyzer" ReferenceOutputAssembly="false"
如下所示:

<Project Sdk="Microsoft.NET.Sdk">

	<PropertyGroup>
		<OutputType>Exe</OutputType>
		<TargetFramework>net8.0</TargetFramework>
		<ImplicitUsings>enable</ImplicitUsings>
		<Nullable>enable</Nullable>
	</PropertyGroup>

	<ItemGroup>
		<ProjectReference Include="..\HelloWorld.Analysis\HelloWorld.Analysis.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
	</ItemGroup>

</Project>

实现Generator

在HelloWorld.Analysis中添加HelloSourceGenerator类。继承并实现ISourceGenerator接口。并且需要在类上加上Generator特性标签。
然后再Exceute中实现我们的代码生成逻辑。

using Microsoft.CodeAnalysis;

namespace HelloWorld.Analysis
{
    [Generator]
    public class HelloSourceGenerator : ISourceGenerator
    {
        public void Execute(GeneratorExecutionContext context)
        {
            var mainMethod = context.Compilation.GetEntryPoint(context.CancellationToken);

            string source = $@"// <auto-generated/>
using System;

namespace {mainMethod.ContainingNamespace.ToDisplayString()}
{{
    public static partial class {mainMethod.ContainingType.Name}
    {{
        static partial void Hello(string name) =>
            Console.WriteLine($""Hello: '{{name}}'"");
    }}
}}
";
            var typeName = mainMethod.ContainingType.Name;

            context.AddSource($"{typeName}.g.cs", source);
        }

        public void Initialize(GeneratorInitializationContext context)
        {
        }
    }
}

在上面代码中,通过Compilation获取Program程序入口的信息。包括命名空间,类名等等等。最后AddSource($"{typeName}.g.cs", source);表示我们把代码生成到.g.cs后缀的文件中。

编译

接下来启动编译项目,在HelloWorld的依赖项的分析器中会出现一个Program.g.cs文件。
image.png
双击打开可以看到生成的代码。并且会提示该文件是自动生成的,无法编辑。
可以看到,文件中我们实现了部分类Program中的部分方法Hello。
image.png

运行项目

启动项目,可以看到我们成功输出由Source Genertor生成的Hello方法的实现。
image.png

注意事项

细心的同学可能会看到我们编译的时候会出现一个警告:
warning RS1036: “HelloWorld.Analysis.HelloSourceGenerator”: 包含分析器或源生成器的项目应指定属性“
true

建议我们在项目中添加EnforceExtendedAnalyzerRules的属性。
当我们添加这个属性后这个警告就会消失。

<Project Sdk="Microsoft.NET.Sdk">

	<PropertyGroup>
		<TargetFramework>netstandard2.0</TargetFramework>
		<LangVersion>latest</LangVersion>
		<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
	</PropertyGroup>

	<ItemGroup>
		<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.2.0" PrivateAssets="all" />
		<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
	</ItemGroup>
</Project>

设置 EnforceExtendedAnalyzerRules 为 true 的作用就是提供 API 禁用分析功能,防止写出分析器不支持的代码。设置 EnforceExtendedAnalyzerRules 为 true 时,有部分的 API 将会被提示不可用。具体API可以看:
https://raw.githubusercontent.com/dotnet/roslyn-analyzers/2b6ab8d727ce73a78bcbf026ac75ea8a7c804daf/src/Microsoft.CodeAnalysis.Analyzers/Core/AnalyzerBannedSymbols.txt

Debug

前面我们直接编译就生成了代码,打断点也不会触发。那么我们如何调试SourceGenerator呢?
可以使用Debugger.Launch();来触发调试。
在我们的运行代码中加入这一行。在编译时会触发调试提示。

using Microsoft.CodeAnalysis;
using System.Diagnostics;

namespace HelloWorld.Analysis
{
    [Generator]
    public class HelloSourceGenerator : ISourceGenerator
    {
        public void Execute(GeneratorExecutionContext context)
        {
            Debugger.Launch(); //触发Debug
            var mainMethod = context.Compilation.GetEntryPoint(context.CancellationToken);

            string source = $@"// <auto-generated/>
using System;

namespace {mainMethod.ContainingNamespace.ToDisplayString()}
{{
    public static partial class {mainMethod.ContainingType.Name}
    {{
        static partial void Hello(string name) =>
            Console.WriteLine($""Hello: '{{name}}'"");
    }}
}}
";
            var typeName = mainMethod.ContainingType.Name;

            context.AddSource($"{typeName}.g.cs", source);
        }

        public void Initialize(GeneratorInitializationContext context)
        {
        }
    }
}

加入代码后,重新执行项目编译操作。会弹出Debugger提示。
image.png
点击OK即可进入调试模式。
image.png
如果不需要点击Cancel则可以跳过。

结语

本文初步的了解了SourceGenerator的功能以及使用和调试的方式,后面的文章我们再来逐步深入学习。