tunerconfig
Description
The tunerconfig
object creates a tuner configuration for a fusion
filter used to tune the filter for reduced estimation error.
Creation
Syntax
Description
creates a config
= tunerconfig(filterName
)tunerconfig
object controlling the optimization algorithm of
the tune function of the fusion filter by specifying a filter name.
creates a config
= tunerconfig(filter
)tunerconfig
object controlling the optimization algorithm of
the tune function of the fusion filter by specifying a filter object.
configures the created config
= tunerconfig(filterName
,Name,Value
)tunerconfig
object properties using one or more
name-value pair arguments. Name
is a property name and
Value
is the corresponding value. Name
must appear
inside quotes. You can specify several name-value pair arguments in any order as
Name1,Value1,...,NameN,ValueN
. Any unspecified properties take
default values.
For example, tunerconfig('imufilter','MaxIterations',3)
create a
tunerconfig
object for the imufilter
object with the a
maximum of three allowed iterations.
Input Arguments
filterName
— Fusion filter name
'imufilter'
| 'ahrsfilter'
| 'ahrs10filter'
| 'insfilterAsync'
| 'insfilterMARG'
| 'insfitlerErrorState'
| 'insfilterNonholonomic'
Fusion filter name, specified as one of these options:
'imufilter'
'ahrsfilter'
'ahrs10filter'
'insfilterAsync'
'insfilterMARG'
'insfitlerErrorState'
'insfilterNonholonomic'
filter
— Fusion filter
fusion filter object
Fusion filter, specified as one of these fusion filter objects:
.
Properties
Filter
— Class name of filter
string
This property is read-only.
Class name of filter, specified as a string. Its value is one of these strings:
"imufilter"
"ahrsfilter"
"ahrs10filter"
"insfilterAsync"
"insfilterMARG"
"insfitlerErrorState"
"insfilterNonholonomic"
TunableParameters
— Tunable parameters
array of string (default) | cell array
Tunable parameters, specified as an array of strings or a cell array.
If you want to tune all the elements in each parameter together (scaling up or down all the elements in a process noise matrix for example), then specify the property as an array of strings. Each string corresponds to a property name.
For filter objects other than the
insEKF
object, this is the default option. With the default option, the property contains all the tunable parameter names as an array of strings. Each string is a tunable property name of the fusion filter.If you want to tune a subset of elements for at least one noise parameter, specify it as a cell array. The number of cells is the number of parameters that you want to tune.
You can specify any cell element as a character vector, representing the property that you want to tune. In this case, the filter tunes all the elements in the property together.
You can also specify any cell element as a 1-by-2 cell array, in which the first cell is a character vector, representing the property that you want tune. The second cell in the cell array is a vector of indices, representing the elements that you want to tune in the property. These indices are column-based indices.
This is default option for the
insEKF
object.For example, running the following:
and you can obtain:>> filter = insEKF; config = tunerconfig(filter); tunable = config.TunableParameters
tunable = 1×3 cell array {1×2 cell} {'AccelerometerNoise'} {'GyroscopeNoise'} >> firstCell = tunable{1} firstCell = 1×2 cell array {'AdditiveProcessNoise'} {[1 15 29 43 57 71 85 99 113 127 141 155 169]}
In the filter, the additive process noise matrix is a 13-by-13 matrices, and the column-based indices represent all the diagonal elements of the matrix.
Example: ["AccelerometerNoise" "GyroscopeNoise"
]
StepForward
— Factor of forward step
1.1
(default) | scalar larger than 1
Factor of a forward step, specified as a scalar larger than 1. During the tuning process, the tuner increases or decreases the noise parameters to achieve smaller estimation errors. This property specifies the ratio of parameter increase during a parameter increase step.
StepBackward
— Factor of backward step
0.5
(default) | scalar in range (0,1)
Factor of a backward step, specified as a scalar in the range of (0,1). During the tuning process, the tuner increases or decreases the noise parameters to achieve smaller estimation errors. This property specifies the factor of parameter decrease during a parameter decrease step.
MaxIterations
— Maximum number of iterations
20
(default) | positive integer
Maximum number of iterations allowed by the tuning algorithm, specified as a positive integer.
ObjectiveLimit
— Cost at which to stop tuning process
0.1
(default) | positive scalar
Cost at which to stop the tuning process, specified as a positive scalar.
FunctionTolerance
— Minimum change in cost to continue tuning
0
(default) | nonnegative scalar
Minimum change in cost to continue tuning, specified as a nonnegative scalar. If the change in cost is smaller than the specified tolerance, the tuning process stops.
Display
— Enable showing the iteration details
"iter"
(default) | "none"
Enable showing the iteration details, specified as "iter"
or
"none"
. When specified as:
"iter"
— The program shows the tuned parameter details in each iteration in the Command Window."none"
— The program does not show any tuning information.
Cost
— Metric for evaluating filter performance
"RMS"
(default) | "Custom"
Metric for evaluating filter performance, specified as "RMS"
or
"Custom"
. When specified as:
"RMS"
— The program optimizes the root-mean-squared (RMS) error between the estimate and the truth."Custom"
— The program optimizes the filter performance by using a customized cost function specified by theCustomCostFcn
property.
CustomCostFcn
— Customized cost function
[]
(default) | function handle
Customized cost function, specified as a function handle.
Dependencies
To enable this property, set the Cost
property to
'Custom'
.
OutputFcn
— Output function called at each iteration
[]
(default) | function handle
Output function called at each iteration, specified as a function handle. The function must use the following syntax:
stop = myOutputFcn(params,tunerValues)
params
is a structure of the current best estimate of each
parameter at the end of the current iteration. tunerValues
is a
structure containing information of the tuner configuration, sensor data, and truth
data. It has these fields:
Field Name | Description |
---|---|
Iteration | Iteration count of the tuner, specified as a positive integer |
SensorData | Sensor data input to the tune function |
GroundTruth | Ground truth input to the tune function |
Configuration | tunerconfig object used for
tuning |
Cost | Tuning cost at the end of the current iteration |
Tip
You can use the built-in function tunerPlotPose
to
visualize the truth data and the estimates for most of your tuning applications. See
the Visualize Tuning Results Using tunerPlotPose example for details.
Examples
Create Tunerconfig Object and Show Tunable Parameters
Create a tunerconfig object for the insfilterAsync
object.
config = tunerconfig('insfilterAsync')
config = tunerconfig with properties: Filter: "insfilterAsync" TunableParameters: ["AccelerometerNoise" "GyroscopeNoise" "MagnetometerNoise" "GPSPositionNoise" "GPSVelocityNoise" "QuaternionNoise" "AngularVelocityNoise" "PositionNoise" "VelocityNoise" ... ] (1x14 string) StepForward: 1.1000 StepBackward: 0.5000 MaxIterations: 20 ObjectiveLimit: 0.1000 FunctionTolerance: 0 Display: iter Cost: RMS OutputFcn: []
Display the default tunable parameters.
config.TunableParameters
ans = 1x14 string
"AccelerometerNoise" "GyroscopeNoise" "MagnetometerNoise" "GPSPositionNoise" "GPSVelocityNoise" "QuaternionNoise" "AngularVelocityNoise" "PositionNoise" "VelocityNoise" "AccelerationNoise" "GyroscopeBiasNoise" "AccelerometerBiasNoise" "GeomagneticVectorNoise" "MagnetometerBiasNoise"
Tune insfilterAsync
to Optimize Pose Estimate
Load the recorded sensor data and ground truth data.
load('insfilterAsyncTuneData.mat');
Create timetables for the sensor data and the truth data.
sensorData = timetable(Accelerometer, Gyroscope, ... Magnetometer, GPSPosition, GPSVelocity, 'SampleRate', 100); groundTruth = timetable(Orientation, Position, ... 'SampleRate', 100);
Create an insfilterAsync
filter object that has a few noise properties.
filter = insfilterAsync('State', initialState, ... 'StateCovariance', initialStateCovariance, ... 'AccelerometerBiasNoise', 1e-7, ... 'GyroscopeBiasNoise', 1e-7, ... 'MagnetometerBiasNoise', 1e-7, ... 'GeomagneticVectorNoise', 1e-7);
Create a tuner configuration object for the filter. Set the maximum iterations to two. Also, set the tunable parameters as the unspecified properties.
config = tunerconfig('insfilterAsync','MaxIterations',8); config.TunableParameters = setdiff(config.TunableParameters, ... {'GeomagneticVectorNoise', 'AccelerometerBiasNoise', ... 'GyroscopeBiasNoise', 'MagnetometerBiasNoise'}); config.TunableParameters
ans = 1×10 string
"AccelerationNoise" "AccelerometerNoise" "AngularVelocityNoise" "GPSPositionNoise" "GPSVelocityNoise" "GyroscopeNoise" "MagnetometerNoise" "PositionNoise" "QuaternionNoise" "VelocityNoise"
Use the tuner noise function to obtain a set of initial sensor noises used in the filter.
measNoise = tunernoise('insfilterAsync')
measNoise = struct with fields:
AccelerometerNoise: 1
GyroscopeNoise: 1
MagnetometerNoise: 1
GPSPositionNoise: 1
GPSVelocityNoise: 1
Tune the filter and obtain the tuned parameters.
tunedParams = tune(filter,measNoise,sensorData,groundTruth,config);
Iteration Parameter Metric _________ _________ ______ 1 AccelerationNoise 2.1345 1 AccelerometerNoise 2.1264 1 AngularVelocityNoise 1.9659 1 GPSPositionNoise 1.9341 1 GPSVelocityNoise 1.8420 1 GyroscopeNoise 1.7589 1 MagnetometerNoise 1.7362 1 PositionNoise 1.7362 1 QuaternionNoise 1.7218 1 VelocityNoise 1.7218 2 AccelerationNoise 1.7190 2 AccelerometerNoise 1.7170 2 AngularVelocityNoise 1.6045 2 GPSPositionNoise 1.5948 2 GPSVelocityNoise 1.5323 2 GyroscopeNoise 1.4803 2 MagnetometerNoise 1.4703 2 PositionNoise 1.4703 2 QuaternionNoise 1.4632 2 VelocityNoise 1.4632 3 AccelerationNoise 1.4596 3 AccelerometerNoise 1.4548 3 AngularVelocityNoise 1.3923 3 GPSPositionNoise 1.3810 3 GPSVelocityNoise 1.3322 3 GyroscopeNoise 1.2998 3 MagnetometerNoise 1.2976 3 PositionNoise 1.2976 3 QuaternionNoise 1.2943 3 VelocityNoise 1.2943 4 AccelerationNoise 1.2906 4 AccelerometerNoise 1.2836 4 AngularVelocityNoise 1.2491 4 GPSPositionNoise 1.2258 4 GPSVelocityNoise 1.1880 4 GyroscopeNoise 1.1701 4 MagnetometerNoise 1.1698 4 PositionNoise 1.1698 4 QuaternionNoise 1.1688 4 VelocityNoise 1.1688 5 AccelerationNoise 1.1650 5 AccelerometerNoise 1.1569 5 AngularVelocityNoise 1.1454 5 GPSPositionNoise 1.1100 5 GPSVelocityNoise 1.0778 5 GyroscopeNoise 1.0709 5 MagnetometerNoise 1.0675 5 PositionNoise 1.0675 5 QuaternionNoise 1.0669 5 VelocityNoise 1.0669 6 AccelerationNoise 1.0634 6 AccelerometerNoise 1.0549 6 AngularVelocityNoise 1.0549 6 GPSPositionNoise 1.0180 6 GPSVelocityNoise 0.9866 6 GyroscopeNoise 0.9810 6 MagnetometerNoise 0.9775 6 PositionNoise 0.9775 6 QuaternionNoise 0.9768 6 VelocityNoise 0.9768 7 AccelerationNoise 0.9735 7 AccelerometerNoise 0.9652 7 AngularVelocityNoise 0.9652 7 GPSPositionNoise 0.9283 7 GPSVelocityNoise 0.8997 7 GyroscopeNoise 0.8947 7 MagnetometerNoise 0.8920 7 PositionNoise 0.8920 7 QuaternionNoise 0.8912 7 VelocityNoise 0.8912 8 AccelerationNoise 0.8885 8 AccelerometerNoise 0.8811 8 AngularVelocityNoise 0.8807 8 GPSPositionNoise 0.8479 8 GPSVelocityNoise 0.8238 8 GyroscopeNoise 0.8165 8 MagnetometerNoise 0.8165 8 PositionNoise 0.8165 8 QuaternionNoise 0.8159 8 VelocityNoise 0.8159
Fuse the sensor data using the tuned filter.
dt = seconds(diff(groundTruth.Time)); N = size(sensorData,1); qEst = quaternion.zeros(N,1); posEst = zeros(N,3); % Iterate the filter for prediction and correction using sensor data. for ii=1:N if ii ~= 1 predict(filter, dt(ii-1)); end if all(~isnan(Accelerometer(ii,:))) fuseaccel(filter,Accelerometer(ii,:), ... tunedParams.AccelerometerNoise); end if all(~isnan(Gyroscope(ii,:))) fusegyro(filter, Gyroscope(ii,:), ... tunedParams.GyroscopeNoise); end if all(~isnan(Magnetometer(ii,1))) fusemag(filter, Magnetometer(ii,:), ... tunedParams.MagnetometerNoise); end if all(~isnan(GPSPosition(ii,1))) fusegps(filter, GPSPosition(ii,:), ... tunedParams.GPSPositionNoise, GPSVelocity(ii,:), ... tunedParams.GPSVelocityNoise); end [posEst(ii,:), qEst(ii,:)] = pose(filter); end
Compute the RMS errors.
orientationError = rad2deg(dist(qEst, Orientation)); rmsorientationError = sqrt(mean(orientationError.^2))
rmsorientationError = 2.7801
positionError = sqrt(sum((posEst - Position).^2, 2)); rmspositionError = sqrt(mean( positionError.^2))
rmspositionError = 0.5966
Visualize the results.
figure(); t = (0:N-1)./ groundTruth.Properties.SampleRate; subplot(2,1,1) plot(t, positionError, 'b'); title("Tuned insfilterAsync" + newline + "Euclidean Distance Position Error") xlabel('Time (s)'); ylabel('Position Error (meters)') subplot(2,1,2) plot(t, orientationError, 'b'); title("Orientation Error") xlabel('Time (s)'); ylabel('Orientation Error (degrees)');
Tune imufilter
to Optimize Orientation Estimate
Load recorded sensor data and ground truth data.
ld = load('imufilterTuneData.mat'); qTrue = ld.groundTruth.Orientation; % true orientation
Create an imufilter
object and fuse the filter with the sensor data.
fuse = imufilter;
qEstUntuned = fuse(ld.sensorData.Accelerometer, ...
ld.sensorData.Gyroscope);
Create a tunerconfig
object and tune the imufilter to improve the orientation estimate.
cfg = tunerconfig('imufilter');
tune(fuse, ld.sensorData, ld.groundTruth, cfg);
Iteration Parameter Metric _________ _________ ______ 1 AccelerometerNoise 0.1149 1 GyroscopeNoise 0.1146 1 GyroscopeDriftNoise 0.1146 1 LinearAccelerationNoise 0.1122 1 LinearAccelerationDecayFactor 0.1103 2 AccelerometerNoise 0.1102 2 GyroscopeNoise 0.1098 2 GyroscopeDriftNoise 0.1098 2 LinearAccelerationNoise 0.1070 2 LinearAccelerationDecayFactor 0.1053 3 AccelerometerNoise 0.1053 3 GyroscopeNoise 0.1048 3 GyroscopeDriftNoise 0.1048 3 LinearAccelerationNoise 0.1016 3 LinearAccelerationDecayFactor 0.1002 4 AccelerometerNoise 0.1001 4 GyroscopeNoise 0.0996 4 GyroscopeDriftNoise 0.0996 4 LinearAccelerationNoise 0.0962 4 LinearAccelerationDecayFactor 0.0950 5 AccelerometerNoise 0.0950 5 GyroscopeNoise 0.0943 5 GyroscopeDriftNoise 0.0943 5 LinearAccelerationNoise 0.0910 5 LinearAccelerationDecayFactor 0.0901 6 AccelerometerNoise 0.0900 6 GyroscopeNoise 0.0893 6 GyroscopeDriftNoise 0.0893 6 LinearAccelerationNoise 0.0862 6 LinearAccelerationDecayFactor 0.0855 7 AccelerometerNoise 0.0855 7 GyroscopeNoise 0.0848 7 GyroscopeDriftNoise 0.0848 7 LinearAccelerationNoise 0.0822 7 LinearAccelerationDecayFactor 0.0818 8 AccelerometerNoise 0.0817 8 GyroscopeNoise 0.0811 8 GyroscopeDriftNoise 0.0811 8 LinearAccelerationNoise 0.0791 8 LinearAccelerationDecayFactor 0.0789 9 AccelerometerNoise 0.0788 9 GyroscopeNoise 0.0782 9 GyroscopeDriftNoise 0.0782 9 LinearAccelerationNoise 0.0769 9 LinearAccelerationDecayFactor 0.0768 10 AccelerometerNoise 0.0768 10 GyroscopeNoise 0.0762 10 GyroscopeDriftNoise 0.0762 10 LinearAccelerationNoise 0.0754 10 LinearAccelerationDecayFactor 0.0753 11 AccelerometerNoise 0.0753 11 GyroscopeNoise 0.0747 11 GyroscopeDriftNoise 0.0747 11 LinearAccelerationNoise 0.0741 11 LinearAccelerationDecayFactor 0.0740 12 AccelerometerNoise 0.0740 12 GyroscopeNoise 0.0734 12 GyroscopeDriftNoise 0.0734 12 LinearAccelerationNoise 0.0728 12 LinearAccelerationDecayFactor 0.0728 13 AccelerometerNoise 0.0728 13 GyroscopeNoise 0.0721 13 GyroscopeDriftNoise 0.0721 13 LinearAccelerationNoise 0.0715 13 LinearAccelerationDecayFactor 0.0715 14 AccelerometerNoise 0.0715 14 GyroscopeNoise 0.0706 14 GyroscopeDriftNoise 0.0706 14 LinearAccelerationNoise 0.0700 14 LinearAccelerationDecayFactor 0.0700 15 AccelerometerNoise 0.0700 15 GyroscopeNoise 0.0690 15 GyroscopeDriftNoise 0.0690 15 LinearAccelerationNoise 0.0684 15 LinearAccelerationDecayFactor 0.0684 16 AccelerometerNoise 0.0684 16 GyroscopeNoise 0.0672 16 GyroscopeDriftNoise 0.0672 16 LinearAccelerationNoise 0.0668 16 LinearAccelerationDecayFactor 0.0667 17 AccelerometerNoise 0.0667 17 GyroscopeNoise 0.0655 17 GyroscopeDriftNoise 0.0655 17 LinearAccelerationNoise 0.0654 17 LinearAccelerationDecayFactor 0.0654 18 AccelerometerNoise 0.0654 18 GyroscopeNoise 0.0641 18 GyroscopeDriftNoise 0.0641 18 LinearAccelerationNoise 0.0640 18 LinearAccelerationDecayFactor 0.0639 19 AccelerometerNoise 0.0639 19 GyroscopeNoise 0.0627 19 GyroscopeDriftNoise 0.0627 19 LinearAccelerationNoise 0.0627 19 LinearAccelerationDecayFactor 0.0624 20 AccelerometerNoise 0.0624 20 GyroscopeNoise 0.0614 20 GyroscopeDriftNoise 0.0614 20 LinearAccelerationNoise 0.0613 20 LinearAccelerationDecayFactor 0.0613
Fuse the sensor data again using the tuned filter.
qEstTuned = fuse(ld.sensorData.Accelerometer, ...
ld.sensorData.Gyroscope);
Compare the tuned and untuned filter RMS error performances.
dUntuned = rad2deg(dist(qEstUntuned, qTrue)); dTuned = rad2deg(dist(qEstTuned, qTrue)); rmsUntuned = sqrt(mean(dUntuned.^2))
rmsUntuned = 6.5864
rmsTuned = sqrt(mean(dTuned.^2))
rmsTuned = 3.5098
Visualize the results.
N = numel(dUntuned); t = (0:N-1)./ fuse.SampleRate; plot(t, dUntuned, 'r', t, dTuned, 'b'); legend('Untuned', 'Tuned'); title('imufilter - Tuned vs Untuned Error') xlabel('Time (s)'); ylabel('Orientation Error (degrees)');
Save Tuned Parameters in MAT File Using Output Function
Load the recorded sensor data and ground truth data.
load('insfilterAsyncTuneData.mat');
Create timetables for the sensor data and the truth data.
sensorData = timetable(Accelerometer, Gyroscope, ... Magnetometer, GPSPosition, GPSVelocity, 'SampleRate', 100); groundTruth = timetable(Orientation, Position, ... 'SampleRate', 100);
Create an insfilterAsync
filter object that has a few noise properties.
filter = insfilterAsync('State', initialState, ... 'StateCovariance', initialStateCovariance, ... 'AccelerometerBiasNoise', 1e-7, ... 'GyroscopeBiasNoise', 1e-7, ... 'MagnetometerBiasNoise', 1e-7, ... 'GeomagneticVectorNoise', 1e-7);
Create a tuner configuration object for the filter. Define the OutputFcn
property as a customized function, myOutputFcn
, which saves the latest tuned parameters in a MAT file.
config = tunerconfig('insfilterAsync', ... 'MaxIterations',5, ... 'Display','none', ... 'OutputFcn', @myOutputFcn); config.TunableParameters = setdiff(config.TunableParameters, ... {'GeomagneticVectorNoise', 'AccelerometerBiasNoise', ... 'GyroscopeBiasNoise', 'MagnetometerBiasNoise'}); config.TunableParameters
ans = 1x10 string
"AccelerationNoise" "AccelerometerNoise" "AngularVelocityNoise" "GPSPositionNoise" "GPSVelocityNoise" "GyroscopeNoise" "MagnetometerNoise" "PositionNoise" "QuaternionNoise" "VelocityNoise"
Use the tuner noise function to obtain a set of initial sensor noises used in the filter.
measNoise = tunernoise('insfilterAsync')
measNoise = struct with fields:
AccelerometerNoise: 1
GyroscopeNoise: 1
MagnetometerNoise: 1
GPSPositionNoise: 1
GPSVelocityNoise: 1
Tune the filter and obtain the tuned parameters.
tunedParams = tune(filter,measNoise,sensorData,groundTruth,config);
Display the save parameters using the saved file.
fileObject = matfile('myfile.mat');
fileObject.params
ans = struct with fields:
AccelerationNoise: [88.8995 88.8995 88.8995]
AccelerometerBiasNoise: [1.0000e-07 1.0000e-07 1.0000e-07]
AccelerometerNoise: 0.7942
AngularVelocityNoise: [0.0089 0.0089 0.0089]
GPSPositionNoise: 1.1664
GPSVelocityNoise: 0.5210
GeomagneticVectorNoise: [1.0000e-07 1.0000e-07 1.0000e-07]
GyroscopeBiasNoise: [1.0000e-07 1.0000e-07 1.0000e-07]
GyroscopeNoise: 0.5210
MagnetometerBiasNoise: [1.0000e-07 1.0000e-07 1.0000e-07]
MagnetometerNoise: 1.0128
PositionNoise: [5.2100e-07 5.2100e-07 5.2100e-07]
QuaternionNoise: [1.3239e-06 1.3239e-06 1.3239e-06 1.3239e-06]
ReferenceLocation: [0 0 0]
State: [28x1 double]
StateCovariance: [28x28 double]
VelocityNoise: [6.3678e-07 6.3678e-07 6.3678e-07]
The output function
function stop = myOutputFcn(params, ~) save('myfile.mat','params'); % overwrite the file with latest stop = false; end
Version History
Introduced in R2020b
See Also
insfilterAsync
| insfilterNonholonomic
| insfilterMARG
| insfilterErrorState
| ahrsfilter
| ahrs10filter
| imufilter
MATLAB Command
You clicked a link that corresponds to this MATLAB command:
Run the command by entering it in the MATLAB Command Window. Web browsers do not support MATLAB commands.
Select a Web Site
Choose a web site to get translated content where available and see local events and offers. Based on your location, we recommend that you select: .
You can also select a web site from the following list
How to Get Best Site Performance
Select the China site (in Chinese or English) for best site performance. Other MathWorks country sites are not optimized for visits from your location.
Americas
- América Latina (Español)
- Canada (English)
- United States (English)
Europe
- Belgium (English)
- Denmark (English)
- Deutschland (Deutsch)
- España (Español)
- Finland (English)
- France (Français)
- Ireland (English)
- Italia (Italiano)
- Luxembourg (English)
- Netherlands (English)
- Norway (English)
- Österreich (Deutsch)
- Portugal (English)
- Sweden (English)
- Switzerland
- United Kingdom (English)
Asia Pacific
- Australia (English)
- India (English)
- New Zealand (English)
- 中国
- 日本Japanese (日本語)
- 한국Korean (한국어)